DynoStudio — Thiết kế production, PostgreSQL, LangGraph, API và WebUI
Ngày: 13/09/2026. Trạng thái: đề xuất để review, chưa triển khai.
Đây là đặc tả triển khai cho modules/studio và phần Studio của modules/web. Tài liệu tập hợp quyết định dữ liệu, API, workflow, vận hành và acceptance gates vào một file. DDL bên dưới là thiết kế tham chiếu cần chuyển thành migration và kiểm thử PostgreSQL; không phải migration đã chạy. Các mức tải, timeout và RPO/RTO là mục tiêu khởi đầu cần đo trước production.
- Phạm vi và baseline
- Các quyết định được sửa sau review
- Topology và code ownership
- Mô hình dữ liệu và invariants
- PostgreSQL schema
- Views, indexes và truy vấn
- Transaction, concurrency và idempotency
- LangGraph và durable state
- API contract
- Script release và publish protocol
- WebUI tái sử dụng Office Tour
- Production operations
- Migration và rollback
- Implementation plan và acceptance
- Test matrix
- Nguồn tham chiếu và quyết định cần review
DynoStudio tạo tour để publish presentation script: chọn địa điểm, source, audience, ngôn ngữ; generate/refine; human edit/review/approve; phát hành script cùng source snapshots và approved prompt pack. Runtime import artifact, bind navigation và thực hiện presentation qua TTS riêng.
WebUI phải cho kiểm thử đầy đủ authoring → publish → preview mà không cần robot hoặc Runtime đang chạy. Catalog fixture hợp lệ và fake LLM là hai adapter dùng cho dev/CI; giao diện phải phân biệt môi trường fixture với kết nối thật.
Không có offline audio generation, WAV, voice credentials hoặc audio approval trong Studio. Không đưa runtime activation, SLAM graph/map editor, microphone, LiveKit hay Q&A execution vào scope này. Preview là text; browser speech nếu thêm sau là tiện ích riêng, không phải release asset hoặc tiêu chí production.
Baseline đã khảo sát:
| Nguồn | Hiện trạng và cách dùng |
|---|---|
modules/studio/README.md, pyproject.toml, workflows/* | Scaffold, dependency list còn rỗng, graphs chưa implement |
modules/web/src/app/App.tsx | Shell React, chưa API/auth |
Office Tour frontend/src/features/tour-wizard/* | Tái sử dụng UX setup/stops/source/speech; thay save/audio bằng review/publish |
Office Tour storage/tour_drafts.py | Có expected revision, nhưng draft stops bị thay wholesale; đọc source từ current document |
Office Tour storage/migrations/001_initial.sql, 004_tour_drafts.sql | Release gắn checkpoint/map và render status; cần schema Studio mới |
Office Tour document_beats, tour_stop_segments | Cần giữ essential, cue, ordered composition và shared content |
architecture/04, 07, 08 | Ownership Studio/Runtime, module architecture, LangGraph bắt buộc |
Mục tiêu parity là giữ nội dung, thứ tự stop/segment, source provenance, audience tùy chỉnh, tiếng Việt/Anh, thao tác refine/edit và essential policy. Thời lượng chuyển thành ước lượng đọc; walking/physical readiness thuộc Runtime.
| Điểm chưa đủ trong plan trước | Quyết định trong tài liệu này |
|---|---|
source_revisions UNIQUE(source_id, content_hash) | Bỏ; cho phép A → B → A như revision mới có lịch sử riêng |
draft_revisions UNIQUE(draft_id, content_hash) | Bỏ; revert có thể trùng semantic content nhưng revision/provenance khác |
| Một segment chỉ có một source | Quan hệ nhiều-nhiều segment_sources, FK tới source đã pin cho stop |
| Dedupe mọi job bằng input hash | Input hash để provenance/cache; dedupe request bằng idempotency key. Explicit regenerate được tạo job mới |
| Snapshot catalog digest nằm trong content hash | Binding/map-only changes không đổi semantic content hash; snapshot provenance nằm trong approval digest và artifact |
| UNIQUE release trên content hash toàn site | Một release trên revision; tour khác có thể dùng cùng nội dung |
| UNIQUE payload theo SHA | Bỏ; hai path hợp lệ có thể chứa bytes giống nhau |
| Approval vừa immutable vừa có trường revoke mutable | Approval append-only; revocation là record riêng, re-approval tạo record mới |
Một trạng thái draft.status mô tả mọi thứ | Archive ở identity; review/publication state suy ra theo revision và records. Published revision cũ vẫn tồn tại khi current draft đang sửa |
| Chỉ publish sau khi có row release | Dùng publication intent, staging/verify artifact, rồi transaction finalize release; chỉ finalized release được liệt kê public |
| UUID PK đủ chống sai site | Dùng composite scope FK cho mọi quan hệ nghiệp vụ; service authorization và RLS bổ sung |
| Approval chỉ dựa vào validation tự động | Grounding checker hỗ trợ reviewer, không chứng minh tất cả facts đúng. Warning acknowledgement gắn validation run |
| WebUI phải hiện graph/hash mọi màn hình | Chi tiết kỹ thuật trong Advanced/Diagnostics; màn hình chính ưu tiên nội dung và trạng thái có ý nghĩa |
Một PostgreSQL cluster có thể chứa database dyno_studio và dyno_runtime với roles riêng trong dev. Production Studio không query database Runtime. studio_workflow là schema trong database Studio, dùng credentials/pool riêng. Quyền truy cập schema checkpointer chỉ cấp worker và công cụ recovery hạn chế.
Lựa chọn triển khai: Python 3.12 theo scaffold; FastAPI/Pydantic ở HTTP boundary; Psycopg 3 + explicit SQL repositories; Alembic quản lý versioned migrations và raw SQL; LangGraph + PostgreSQL checkpointer; React/TypeScript/Vite theo project cũ. Chốt exact package versions, Python/image digest và lockfile tại P0; không cài latest khi deploy.
PostgreSQL baseline đề xuất: major 17, minor security patch đã test và pin vào deployment manifest. Đây là lựa chọn compatibility, không phải khẳng định major mới nhất. Không cần PostGIS/vector extension cho authoring v1.
modules/studio/src/dyno_studio/
bootstrap/{settings,resources,wiring}.py
entrypoints/{api,worker,cli}.py
modules/
catalog/ # snapshot importer + catalog queries
sources/ # source identities/revisions
authoring/ # draft, revision, review, approval aggregate
generation/ # jobs, prompts, LLM ports, validation
publishing/ # release contract, object storage, publication
workflows/{generation,refinement,review,publishing}/
persistence/{unit_of_work,migrations}/
Module đủ lớn mới chia domain/application/adapters. Application sở hữu UoW và interfaces; SQL/HTTP/provider clients nằm ở adapters. workflows là LangGraph adapter gọi application. Không import dyno_runtime, office_tour.runtime, speech renderer hoặc robot SDK vào Studio.
#4.1 Aggregate và lifecycle
Identity (drafts, sources) có current pointer/version và archive state. Một lần Save tạo immutable revision đầy đủ, không update rows của revision cũ. Cho phép draft chưa có stop/segment; chỉ review/publish yêu cầu đầy đủ. Không persist từng keystroke: WebUI gom Save/debounce explicit theo settings, có trạng thái unsaved và conflict rõ ràng.
stop_id và segment_id ổn định qua revisions nếu vẫn là cùng occurrence. Copy tour tạo identities mới; duplicate segment tạo segment_id mới. Cho phép cùng place/target xuất hiện lại trong tour bằng stop_id khác. V1 ordinal dùng 1-based cho DB/API/artifact; migration chuyển ordinal cũ theo mapping đã test.
Mỗi draft revision có một locale và audience snapshot hoàn chỉnh, gồm custom description. Source locale được lưu rõ; cross-language generation phải explicit và reviewer nhìn thấy. Script segment là đơn vị text/essential/cue; nếu UI group nhiều đoạn thì group chỉ phục vụ trình bày. Shared content được copy materialized vào revision, kèm provenance; sửa library không tự đổi các tour đã pin.
#4.2 Ba loại hash
| Hash | Nội dung | Mục đích |
|---|---|---|
content_hash | Ordered semantic script, locale/audience/brief, semantic place/target IDs, source content hashes, approved runtime prompts, cues, essential, schema semantics version | Nhận diện nội dung thực tế |
approval_digest | content_hash + exact revision ID + source revision IDs + catalog snapshot identity/digest + policy/prompt pack revisions | Reviewer duyệt đúng bản và provenance |
release_digest | Canonical manifest body với payload inventory, approval metadata và release identity; loại chính trường release_digest | Runtime kiểm tra artifact |
Rebind waypoint/map không sửa script. Nếu chỉ binding mới trong Runtime, không tạo draft revision và không thay hash. Import catalog snapshot mới rồi chủ động áp dụng cho draft tạo revision mới; content_hash có thể giữ nguyên, approval_digest đổi và phải review lại. Nếu semantic description/title dùng trong script thay đổi thì semantic content cũng đổi.
Hash là SHA-256 hex 64 ký tự lowercase. Canonical serializer v1 cần golden fixtures: UTF-8, NFC normalization lúc ingest, LF newlines, object keys sort ổn định, array giữ thứ tự, không NaN/Infinity, không float trong canonical payload. Dùng integer target_seconds và estimated_seconds; chữ số JSON và Unicode escaping quy định thống nhất cho Python/TypeScript. Không trim/sửa text lúc publish sau approval. Không hash ZIP bytes vì metadata ZIP không ổn định.
#4.3 Invariants bắt buộc
- Mọi FK nghiệp vụ cùng tenant/site; UUID không thay authorization.
- Current pointer thuộc đúng source/draft identity; parent revision thuộc cùng draft.
- Target thuộc đúng place và catalog snapshot draft đang pin.
- Source lineage từ segment chỉ tham chiếu source đã pin cho stop, cùng scope.
- Revision và child rows được seal trong cùng transaction; sau seal cấm INSERT/UPDATE/DELETE children.
- Review/approval/publish chỉ dùng revision sealed.
- Approval gắn revision, approval_digest, validation run, policy version, actor từ auth.
- Revoked approval không authorize publication mới; release đã phát hành có lifecycle revocation riêng.
- Một publication intent cho một draft revision; retry reuse intent/release ID.
- Published release và payload inventory immutable; artifact tồn tại/verify trước khi finalization.
- Job result dựa vào revision cũ không tự ghi đè current revision mới.
- Không kiểm tra relational rules bằng CHECK đọc bảng khác. Dùng FK/unique hoặc transaction + constraint trigger. PostgreSQL giải thích giới hạn CHECK xuyên hàng/bảng tại Constraints.
#5.1 Namespaces, types và quy ước dictionary
Các bảng bên dưới nằm trong studio. Các bảng checkpoint do package LangGraph quản lý trong studio_workflow; không tự đoán hoặc copy schema nội bộ thư viện.
CREATE SCHEMA studio;
CREATE SCHEMA studio_workflow;
CREATE DOMAIN studio.sha256_hex AS text
CHECK (VALUE ~ '^[0-9a-f]{64}$');
CREATE DOMAIN studio.nonblank AS text
CHECK (length(btrim(VALUE)) > 0);
Quy ước: S = (tenant_id text, site_id text) NOT NULL; mọi bảng scoped có FK S → sites. ID = UUID do application tạo, không nhận random ID thay auth scope. created_at = timestamptz NOT NULL DEFAULT now(); created_by = authenticated subject text NOT NULL. Cột ? nullable; cột còn lại NOT NULL trừ khi DDL ghi rõ. Mọi PK scoped viết đầy đủ trong migration. Shared control tables tenants/sites là ngoại lệ có chủ đích.
#5.2 Tenant, site và access
| Table | Columns | Constraints |
|---|---|---|
tenants | tenant_id text; name nonblank; disabled_at timestamptz?; created_at | PK tenant_id |
sites | S; name nonblank; default_locale text; archived_at timestamptz?; created_at | PK S; FK tenant_id |
site_memberships | S; subject_id text; role text; granted_by text; created_at; revoked_at? | PK(S,subject_id,role); role author/reviewer/publisher/catalog_admin/site_admin/runtime_reader |
OIDC identity provider là authority về authentication. Membership là local authorization theo site; JWT roles từ provider phải map qua allowlist, không tin role do request body truyền vào. Một người có thể có nhiều role trong dev; production mặc định reviewer khác last content editor, cấu hình bằng versioned approval policy.
#5.3 Catalog read model
| Table | Columns | Constraints |
|---|---|---|
catalog_snapshots | S; snapshot_id uuid; upstream_revision nonblank; schema_version text; digest sha256; imported_at; imported_by; sealed_at? | PK(S,snapshot_id); UQ(S,upstream_revision); UQ(S,digest) |
catalog_places | S; snapshot_id; place_id text; stable_code text; title_i18n jsonb; semantic_description_i18n jsonb; retired boolean | PK(S,snapshot_id,place_id); FK snapshot; UQ(S,snapshot_id,stable_code) |
catalog_targets | S; snapshot_id; target_id text; place_id text; role text; label_i18n jsonb; retired boolean; binding_display jsonb? | PK(S,snapshot_id,target_id); FK exact place; UQ(S,snapshot_id,place_id,target_id) |
binding_display là optional read-only hint có schema version: ví dụ graph label/checkpoint label; không tham gia semantic script/hash, không dùng để chứng nhận route ready. Site/semantic IDs là opaque text vì phải tương thích Runtime contract; reject rỗng và giới hạn 128 ký tự. Source/catalog text hỗ trợ locales rõ ràng, không tự dùng tiếng Việt thay tiếng Anh mà giấu fallback.
Catalog import validate trước rồi insert snapshot + toàn bộ child rows + seal trong một transaction. Same upstream revision/different digest trả conflict. Không cập nhật snapshot cũ thành superseded; newest là query theo imported_at, draft luôn pin snapshot cụ thể. Mapping POI legacy → place/target nằm trong migration manifest.
#5.4 Sources và shared authored content
| Table | Columns | Constraints |
|---|---|---|
sources | S; source_id; source_key nonblank; current_revision_id?; version bigint default 0; archived_at?; created_at/by | PK(S,source_id); UQ(S,source_key); scoped owner-aware current FK |
source_revisions | S; source_id; revision_id; revision_no int; title nonblank; locale text; body nonblank; content_hash sha256; export_policy text; note text; created_at/by | PK(S,revision_id); UQ(S,source_id,revision_no); UQ(S,source_id,revision_id); FK source; revision_no>0; export_policy runtime_allowed/studio_only |
source_place_links | S; source_id; catalog_snapshot_id; place_id | PK(S,source_id,catalog_snapshot_id,place_id); FK source and catalog place |
content_templates | S; template_id; template_key; current_revision_id?; version bigint; archived_at? | PK(S,template_id); UQ(S,template_key) |
content_template_revisions | S; template_id; revision_id; revision_no; title; locale; audience_description; sealed_at?; created_at/by | PK(S,revision_id); UQ(S,template_id,revision_id); UQ(S,template_id,revision_no); FK template |
template_segments | S; template_revision_id; ordinal; text nonblank; essential boolean; gesture_cue jsonb? | PK(S,template_revision_id,ordinal); FK template revision; ordinal>0 |
Source revisions immutable ngay khi insert. Template revision và segments dùng seal giống draft. Khi apply template, copy text/essential/cue vào draft_segments; lưu template revision + ordinal provenance. V1 yêu cầu source references cho factual content, có thể empty cho welcome/transition được classify rõ. API apply-template nhận mapping nguồn đã chọn và copy vào stop_sources/segment_sources, không tự gán nguồn tùy ý.
Nguồn studio_only được dùng trong authoring nội bộ nhưng không được export raw; publish v1 chặn nếu draft yêu cầu source đó trong runtime context. Author phải chọn một source revision được phép export rồi review lại. Không tự bỏ source nhạy cảm khỏi release trong bước publish.
#5.5 Versioned prompts và policies
| Table | Columns | Constraints |
|---|---|---|
prompt_pack_revisions | S; prompt_revision_id; pack_key; version; purpose generation/runtime; locale; templates jsonb; digest sha256; created_at/by | PK(S,prompt_revision_id); UQ(S,pack_key,version) |
policy_revisions | S; policy_revision_id; policy_key; version; schema_version; rules jsonb; digest sha256; created_at/by | PK(S,policy_revision_id); UQ(S,policy_key,version) |
JSONB fields được validate theo strict versioned schema: length limits, unknown fields policy, allowed cue names, duration/word-count rules, approval separation, warning codes. Provider secrets không được lưu trong prompts/policy hoặc artifact. Runtime prompt pack là content đã review, generation prompt là provenance/job input; không tự export system prompt generation.
#5.6 Draft core — DDL tham chiếu
DDL này minh họa scoped FK, ownership và pin snapshot; prerequisite tables phải được tạo theo dictionary trước. UUID không có default để IDs được reserve trước transaction. Tên drafts là tên schema mới, WebUI vẫn hiển thị Tour.
CREATE TABLE studio.drafts (
tenant_id text NOT NULL, site_id text NOT NULL,
draft_id uuid NOT NULL, draft_key studio.nonblank NOT NULL,
current_revision_id uuid, version bigint NOT NULL DEFAULT 0 CHECK (version >= 0),
archived_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(), created_by text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(), updated_by text NOT NULL,
PRIMARY KEY (tenant_id, site_id, draft_id),
UNIQUE (tenant_id, site_id, draft_key),
FOREIGN KEY (tenant_id, site_id) REFERENCES studio.sites(tenant_id, site_id)
);
CREATE TABLE studio.draft_revisions (
tenant_id text NOT NULL, site_id text NOT NULL,
revision_id uuid NOT NULL, draft_id uuid NOT NULL,
revision_no integer NOT NULL CHECK (revision_no > 0), parent_revision_id uuid,
catalog_snapshot_id uuid NOT NULL,
runtime_prompt_revision_id uuid NOT NULL, policy_revision_id uuid NOT NULL,
name studio.nonblank NOT NULL, locale text NOT NULL,
audience_key text, audience_name studio.nonblank NOT NULL,
audience_description studio.nonblank NOT NULL,
target_seconds integer NOT NULL CHECK (target_seconds BETWEEN 300 AND 3600),
content_hash studio.sha256_hex NOT NULL,
approval_digest studio.sha256_hex NOT NULL,
change_reason text NOT NULL, sealed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(), created_by text NOT NULL,
PRIMARY KEY (tenant_id, site_id, revision_id),
UNIQUE (tenant_id, site_id, draft_id, revision_id),
UNIQUE (tenant_id, site_id, draft_id, revision_no),
UNIQUE (tenant_id, site_id, revision_id, catalog_snapshot_id),
UNIQUE (tenant_id, site_id, revision_id, approval_digest),
FOREIGN KEY (tenant_id, site_id, draft_id)
REFERENCES studio.drafts(tenant_id, site_id, draft_id),
FOREIGN KEY (tenant_id, site_id, draft_id, parent_revision_id)
REFERENCES studio.draft_revisions(tenant_id, site_id, draft_id, revision_id),
FOREIGN KEY (tenant_id, site_id, catalog_snapshot_id)
REFERENCES studio.catalog_snapshots(tenant_id, site_id, snapshot_id),
FOREIGN KEY (tenant_id, site_id, runtime_prompt_revision_id)
REFERENCES studio.prompt_pack_revisions(tenant_id, site_id, prompt_revision_id),
FOREIGN KEY (tenant_id, site_id, policy_revision_id)
REFERENCES studio.policy_revisions(tenant_id, site_id, policy_revision_id)
);
ALTER TABLE studio.drafts ADD CONSTRAINT drafts_current_owned
FOREIGN KEY (tenant_id, site_id, draft_id, current_revision_id)
REFERENCES studio.draft_revisions(tenant_id, site_id, draft_id, revision_id)
DEFERRABLE INITIALLY DEFERRED;
CREATE TABLE studio.draft_stops (
tenant_id text NOT NULL, site_id text NOT NULL,
revision_id uuid NOT NULL, stop_id uuid NOT NULL,
catalog_snapshot_id uuid NOT NULL,
ordinal integer NOT NULL CHECK (ordinal > 0),
place_id text NOT NULL, target_id text NOT NULL,
title studio.nonblank NOT NULL,
PRIMARY KEY (tenant_id, site_id, revision_id, stop_id),
UNIQUE (tenant_id, site_id, revision_id, ordinal),
FOREIGN KEY (tenant_id, site_id, revision_id, catalog_snapshot_id)
REFERENCES studio.draft_revisions(tenant_id, site_id, revision_id, catalog_snapshot_id),
FOREIGN KEY (tenant_id, site_id, catalog_snapshot_id, place_id, target_id)
REFERENCES studio.catalog_targets(tenant_id, site_id, snapshot_id, place_id, target_id)
);
CREATE TABLE studio.draft_segments (
tenant_id text NOT NULL, site_id text NOT NULL,
revision_id uuid NOT NULL, stop_id uuid NOT NULL, segment_id uuid NOT NULL,
ordinal integer NOT NULL CHECK (ordinal > 0),
kind text NOT NULL CHECK (kind IN ('factual','welcome','transition','closing')),
text text NOT NULL, essential boolean NOT NULL DEFAULT true,
gesture_cue jsonb,
template_revision_id uuid, template_ordinal integer,
PRIMARY KEY (tenant_id, site_id, revision_id, stop_id, segment_id),
UNIQUE (tenant_id, site_id, revision_id, segment_id),
UNIQUE (tenant_id, site_id, revision_id, stop_id, ordinal),
FOREIGN KEY (tenant_id, site_id, revision_id, stop_id)
REFERENCES studio.draft_stops(tenant_id, site_id, revision_id, stop_id),
FOREIGN KEY (tenant_id, site_id, template_revision_id, template_ordinal)
REFERENCES studio.template_segments(tenant_id, site_id, template_revision_id, ordinal),
CHECK ((template_revision_id IS NULL) = (template_ordinal IS NULL)),
CHECK (gesture_cue IS NULL OR jsonb_typeof(gesture_cue) = 'object')
);
CREATE TABLE studio.stop_sources (
tenant_id text NOT NULL, site_id text NOT NULL,
revision_id uuid NOT NULL, stop_id uuid NOT NULL, source_revision_id uuid NOT NULL,
PRIMARY KEY (tenant_id, site_id, revision_id, stop_id, source_revision_id),
FOREIGN KEY (tenant_id, site_id, revision_id, stop_id)
REFERENCES studio.draft_stops(tenant_id, site_id, revision_id, stop_id),
FOREIGN KEY (tenant_id, site_id, source_revision_id)
REFERENCES studio.source_revisions(tenant_id, site_id, revision_id)
);
CREATE TABLE studio.segment_sources (
tenant_id text NOT NULL, site_id text NOT NULL,
revision_id uuid NOT NULL, stop_id uuid NOT NULL, segment_id uuid NOT NULL,
source_revision_id uuid NOT NULL,
PRIMARY KEY (tenant_id, site_id, revision_id, stop_id, segment_id, source_revision_id),
FOREIGN KEY (tenant_id, site_id, revision_id, stop_id, segment_id)
REFERENCES studio.draft_segments(tenant_id, site_id, revision_id, stop_id, segment_id),
FOREIGN KEY (tenant_id, site_id, revision_id, stop_id, source_revision_id)
REFERENCES studio.stop_sources(tenant_id, site_id, revision_id, stop_id, source_revision_id)
);
Draft có thể chứa empty segment để lưu công việc dở; validation EMPTY_SEGMENT chặn submit review. target_seconds giữ giới hạn 5–60 phút của UI cũ; muốn mở rộng phải sửa policy/API/DB cùng migration. Các estimate không persist thành nội dung authoritative; validation run lưu estimator version và kết quả.
Seal implementation bắt buộc: API không có quyền INSERT trực tiếp revision children. Writer gọi repository transaction/SQL function được giới hạn quyền; lock parent, validate child topology/hash, set sealed_at; deferred trigger kiểm tra tất cả inserted revisions đã sealed lúc commit. Child mutation trigger lock parent FOR UPDATE, reject nếu sealed. Parent trigger chỉ cho unsealed → sealed và cấm mọi update/delete sau đó. Chặn cả INSERT child vào sealed parent, không chỉ UPDATE/DELETE. Sibling source/template/catalog có cùng cơ chế khi cần nhiều rows.
#5.7 Validation, review và approvals
| Table | Columns | Constraints |
|---|---|---|
validation_runs | S; validation_id; revision_id; approval_digest; validator_version; estimator_version; policy_revision_id; status passed/failed; estimated_seconds int; summary jsonb; created_at | PK(S,validation_id); UQ(S,revision_id,approval_digest,validation_id); FK exact revision/digest, policy |
validation_issues | S; validation_id; issue_id; severity error/warning/info; code; scope_path text; message; detail jsonb | PK(S,validation_id,issue_id); FK validation; scope_path JSON pointer into canonical revision |
review_requests | S; review_id; revision_id; validation_id; approval_digest; workflow_id; created_at/by | PK(S,review_id); UQ(S,review_id,revision_id,approval_digest,validation_id); FK validation tuple |
review_actions | S; action_id; review_id; kind comment/request_changes/approve/withdraw; scope_path?; comment; acknowledged_issue_ids jsonb; created_at/by | PK(S,action_id); FK review; append-only |
content_approvals | S; approval_id; review_id; revision_id; approval_digest; validation_id; action_id; reviewer_id; created_at | PK(S,approval_id); UQ(S,approval_id,revision_id,approval_digest); FK exact review tuple; FK action; application verifies action kind/actor |
approval_revocations | S; approval_id; reason nonblank; created_at/by | PK(S,approval_id); FK approval |
Re-approval cùng revision hợp lệ sau revoke tạo approval_id mới. Không mutate historical approval. Publish transaction chọn explicit approval_id và kiểm tra không revoked; approval/revocation endpoints cùng lock draft aggregate để serialize với publication finalization. Historical release lưu approval đã dùng; revoke approval không tự đổi immutable artifact hoặc stop robot. Nếu cần thu hồi release, publisher gọi release-revocation endpoint, Runtime áp dụng policy ở lần import/activation kế tiếp.
Validation đồng bộ deterministic gồm shape, links, text policy, estimates/cues. LLM grounding check async lưu findings trong validation run với model/prompt version; không đánh dấu fact verified chỉ vì LLM đồng ý. Rule severity và trường hợp reviewer được acknowledge warning do policy revision quy định. Error không được override bằng checkbox.
#5.8 Workflow, jobs và execution records
| Table | Columns | Constraints |
|---|---|---|
workflow_runs | S; workflow_id; kind generate/refine/review/publish/catalog_import/validate/verify_artifact; graph_version; state_schema_version; thread_id text; draft_id?; base_revision_id?; status queued/running/waiting_input/succeeded/failed/cancelled/superseded; epoch bigint; lease_owner?; lease_until?; deadline_at?; created_at/by; completed_at? | PK(S,workflow_id); UQ(thread_id); FK draft + owned base revision; epoch>=0; draft/base bắt buộc cho generate/refine/review/publish/validate |
workflow_commands | S; command_id; workflow_id; kind start/resume/cancel; expected_epoch bigint; payload jsonb; status pending/claimed/processed/rejected; created_at/by; processed_at? | PK(S,command_id); FK workflow; resume references durable review action/publication request |
generation_jobs | S; job_id; workflow_id; input_hash; generation_prompt_revision_id; provider; model; parameters jsonb; base_revision_id; result_revision_id?; apply_state none/available/applied/conflict; max_attempts int; estimated_cost?; created_at | PK(S,job_id); UQ(S,workflow_id); FK workflow/prompts/revisions; no unique input_hash |
generation_items | S; job_id; item_id; stop_id?; segment_id?; ordinal; status queued/running/succeeded/failed/cancelled; result jsonb?; error_code? | PK(S,job_id,item_id); FK job; scope IDs must belong to job base revision, checked at request creation |
node_executions | S; workflow_id; node_key; operation_key; attempt int; epoch; input_hash; result_ref?; status started/succeeded/failed/unknown; provider_request_id?; token_usage jsonb?; created_at; completed_at? | PK(S,workflow_id,node_key,operation_key,attempt); FK workflow; dedupe successful logical operation via partial unique |
workflow_events | S; workflow_id; sequence bigint; type; payload jsonb; created_at | PK(S,workflow_id,sequence); FK workflow; sequence assigned under workflow row lock |
background_operations | S; operation_id; workflow_id; kind catalog_import/validate/verify_artifact; input_ref text; input_hash; result_id uuid?; created_at/by | PK(S,operation_id); UQ(S,workflow_id); FK workflow; result type được xác định theo kind |
Auxiliary workflows dùng state variant riêng: catalog import không có draft/base revision; artifact verification pin release_id qua operation input. background_operations là scheduling envelope, không làm polymorphic FK authoritative: result_id được resolve/validate qua typed adapter, các result tables vẫn có đầy đủ scoped FK. Input upload lưu private staging ref có hash/quota; không fetch URL tùy ý từ client. POST auxiliary trả operation_id/workflow_id và status_url; GET catalog-imports/validations/artifact-verifications nhận operation_id, trả execution status và result khi hoàn tất. Internal validation_id/verification_id là ID record kết quả và chỉ xuất hiện sau commit thành công; review yêu cầu validation_id đó.
node_executions lưu durable result cho node có side effect. Partial unique trên (S,workflow_id,node_key,operation_key) WHERE status='succeeded'. Provider call chưa trả kết quả có thể bị tính phí dù process chết; không hứa exactly-once LLM billing. Reconcile provider request nếu provider hỗ trợ; nếu không, retry theo budget và ghi attempt riêng.
#5.9 Publication và immutable releases
| Table | Columns | Constraints |
|---|---|---|
publication_requests | S; publication_id; workflow_id; draft_id; revision_id; approval_id; reserved_release_id; approval_digest; status queued/building/verifying/finalizing/succeeded/failed/cancelled; storage_prefix; expected_digest?; failure_code?; created_at/by | PK(S,publication_id); UQ(S,revision_id); UQ(S,reserved_release_id); FK workflow, owned draft revision, exact approval tuple |
script_releases | S; release_id; draft_id; revision_id; release_no int; publication_id; approval_id; content_hash; approval_digest; release_digest; schema_version; manifest_ref; published_at; published_by | PK(S,release_id); UQ(S,revision_id); UQ(S,draft_id,release_no); UQ(S,publication_id); FK publication/approval/revision |
release_payloads | S; release_id; path; sha256; size_bytes bigint; media_type; storage_ref | PK(S,release_id,path); FK release; size_bytes>=0; no unique sha256 |
release_revocations | S; release_id; reason; created_at/by | PK(S,release_id); FK release; append-only |
artifact_verifications | S; verification_id; release_id; verifier_version; status passed/failed; issues jsonb; created_at | PK(S,verification_id); FK release |
Release + inventory được finalize/seal một transaction; trigger cấm thêm payload sau finalize. Version release_no được cấp dưới draft lock trong finalize; không dùng MAX()+1 ngoài lock. Release identity khác artifact download format: ZIP/tar là transport, JSON inventory là canonical contract. Revoked release vẫn có metadata/history; runtime_reader không được download mới trừ administrative diagnostic policy.
#5.10 Infrastructure tables
| Table | Columns | Constraints |
|---|---|---|
idempotency_keys | S; actor_id; operation; key; request_hash; status in_progress/completed; response_status?; response_body?; resource_id?; created_at; expires_at | PK(S,actor_id,operation,key); response body bounded/redacted |
outbox_events | S; event_id; aggregate_kind; aggregate_id; type; schema_version; payload jsonb; status pending/claimed/delivered/dead_letter; attempts; next_attempt_at; lease_until?; created_at | PK(S,event_id); event_id downstream dedupe |
audit_events | S; audit_id uuid; actor_id; action; resource_kind; resource_id; revision_id?; request_id; detail jsonb; created_at | PK(S,audit_id); append-only; text/secrets excluded by default |
migration_imports | S; batch_id; legacy_kind; legacy_id; legacy_revision text; source_digest; destination_id; result jsonb; created_at | PK(S,batch_id,legacy_kind,legacy_id,legacy_revision); repeat batch idempotent |
Operational tables bounded by retention; immutable content records referenced by releases không cascade-delete. FK mặc định NO ACTION/RESTRICT; archive thay delete trong API v1. Retention purge là admin workflow riêng, có dependency report.
#6.1 Read views
v_draft_heads: identity + current revision, current review/approval/publication flags, latest published release, has_unpublished_changes. Flags không biến published old revision thành current approved.
v_source_heads: source + exact current revision; source out-of-date so sánh head với pinned revision trong draft, không join head khi đọc historical draft body.
v_release_library: releases + optional revocation; metadata từ exact revision, payload count/bytes từ inventory. v_workflow_status: durable workflow row + last event sequence + pending interaction; không đọc raw checkpoint blob trực tiếp từ browser.
CREATE VIEW studio.v_source_heads WITH (security_invoker = true) AS
SELECT s.tenant_id, s.site_id, s.source_id, s.source_key, s.version,
s.archived_at, r.revision_id, r.revision_no, r.title, r.locale,
r.content_hash, r.export_policy, r.created_at
FROM studio.sources s
JOIN studio.source_revisions r
ON (r.tenant_id,r.site_id,r.source_id,r.revision_id) =
(s.tenant_id,s.site_id,s.source_id,s.current_revision_id);
Use regular views ban đầu; materialized view chỉ khi query measurements chứng minh cần và có freshness contract. security_invoker giữ permission/RLS của caller cho view; ownership và FORCE RLS phải test, không giả định bật policy là đủ. PostgreSQL mô tả bypass của owner/superuser tại Row security.
#6.2 Index plan
| Query | Index bổ sung ngoài PK/UNIQUE |
|---|---|
| Draft library | drafts(S,updated_at DESC,draft_id DESC) WHERE archived_at IS NULL |
| Revision history | draft_revisions(S,draft_id,revision_no DESC) |
| Source history | source_revisions(S,source_id,revision_no DESC) |
| Source impact | stop_sources(S,source_revision_id,revision_id) |
| Review queue | review_requests(S,created_at DESC,review_id DESC), approvals(S,revision_id) |
| Release library | script_releases(S,published_at DESC,release_id DESC) |
| Job queue | workflow_runs(status,lease_until,created_at) WHERE status IN ('queued','running') |
| Pending commands | workflow_commands(status,created_at) WHERE status='pending' |
| Outbox | outbox_events(status,next_attempt_at) WHERE status IN ('pending','claimed') |
| Audit | audit_events(S,created_at DESC,audit_id DESC) |
FK referencing columns cần index nếu parent deletion/update/check hoặc joins dùng chúng; PostgreSQL không tự tạo mọi referencing-side FK index. Không GIN toàn bộ JSONB mặc định. Cursor pagination (updated_at,id) limit default 20/max 100; thứ tự có tie-breaker. Explain Analyze trên fixture 10k drafts, 200k revisions và job history trước rollout; đây là dataset kiểm thử, không capacity claim.
#6.3 RLS và role
Production app roles không là table owner, superuser hoặc BYPASSRLS. studio_migrator owns schema và DDL; studio_api, studio_worker, studio_checkpoint, studio_readonly nhận grants tối thiểu. RLS theo site membership ở application + scope context trong transaction. Ví dụ predicate cho scoped table: tenant_id bằng current_setting('app.tenant_id',true) và site_id bằng current_setting('app.site_id',true); WITH CHECK cùng predicate, FORCE RLS.
Thiết lập context bằng parameterized SELECT set_config('app.tenant_id', %s, true) và site tương tự sau BEGIN; scope lấy từ authenticated request. Pool không giữ tenant context qua transaction. RLS này chống query bỏ WHERE trong trusted app; không bảo vệ một compromised app credential tự set tenant tùy ý. Global job dispatcher dùng stored claim function hạn chế quyền hoặc role control-plane riêng; worker chạy nghiệp vụ với scope đã xác minh từ claimed job. Tránh cấp BYPASSRLS toàn DB chỉ để poll queue.
#7.1 Save draft revision
- BEGIN READ COMMITTED; set tenant/site context.
- Lock draft
FOR UPDATE, kiểm tra archive, role và expected version. - Allocate revision_no dưới lock; validate request shape, catalog/source/policy links.
- Insert revision unsealed + stops/segments/source links. Tính lại canonical hashes ở server.
- Validate topology; seal. Cập nhật current_revision_id và version+1.
- Append audit + outbox/workflow command nếu use case cần; commit.
Không gọi LLM/object storage trong transaction. CAS hoặc row lock chống lost update; conflict trả 412 theo HTTP If-Match. Retry deadlock/serialization tối đa 3 lần với jitter cho use case idempotent; không retry business conflict.
#7.2 Job output trong lúc author chỉnh sửa
Job pin base revision. Result là immutable candidate revision có parent=base, chưa chuyển current pointer. Nếu current pointer vẫn bằng base và expected version đúng, explicit apply hoặc configured auto-apply chuyển pointer atomically. Nếu head đã đổi, API báo apply_state=conflict, UI cho diff và tạo revision merge mới. Không tự merge text hoặc xóa bản người viết. V1 default explicit Apply suggestion, kể cả generate-all.
#7.3 Approval race
Submit review pin current revision, digest, completed validation run. Approve lock draft + verify review chưa superseded, current revision vẫn đúng, policy actor/warnings hợp lệ; insert review action + approval + resume command cùng transaction. Reviewer không gửi approved=true để workflow tự tin; worker load approval record từ DB. Nếu source head đổi nhưng draft vẫn pin bản cũ, hiện warning; không tự đổi pinned content. Replacing pinned source phải tạo revision mới và review mới.
#7.4 Idempotency contract
Required cho POST tạo resource/job/review action/approval/publication. Key có phạm vi S + actor + HTTP operation + target resource. Cùng key/body trả cùng resource/response; cùng key/body khác trả 409 IDEMPOTENCY_KEY_REUSED. Request hash bao method/path/canonical body và If-Match. Transaction tạo durable intent và response 202/201 cùng row idempotency; không giữ request in-progress suốt một job nhiều phút.
Retention key mặc định 7 ngày; sau hạn, DB uniqueness của publication/release tiếp tục chống duplicate release. Explicit regenerate dùng key mới. Network retry giữ key cũ. Trường hợp intent tồn tại nhưng job thất bại thì retry workflow qua endpoint retry, reuse operation identity; không tạo release ID khác.
#8.1 Quyết định bắt buộc
LangGraph điều phối generation, refinement, human review và publishing. PostgreSQL checkpointer giữ tiến trình thread qua restart; production không dùng InMemorySaver. thread_id dùng UUID opaque, được map qua workflow_runs và authorize bằng server. Cài đặt/pin/checkpointer setup nằm trong deployment migration job, không chạy schema setup từ mỗi API replica. Khả năng persistence dựa trên LangGraph checkpointers.
| Loại state | Authority | Ví dụ |
|---|---|---|
| Nội dung và quyền duyệt | Studio relational tables | exact revision, approval, release |
| Execution position | LangGraph checkpoint | next node, pending interrupt, retry state |
| Scheduling/recovery | workflow_runs/commands/executions | lease, epoch, result refs, command dedupe |
| Browser state | React local state | unsaved textarea, open tab |
API status được dựng từ durable business/execution rows và reconcile checkpoint. Không có hai owner cùng mutate một workflow thread. LangGraph không làm thay FK, access checks hoặc DB transaction.
#8.2 Typed state đề xuất
class StudioWorkflowState(TypedDict):
workflow_id: str
state_schema_version: int
graph_version: str
tenant_id: str
site_id: str
draft_id: str
base_revision_id: str
candidate_revision_id: str | None
review_id: str | None
approval_id: str | None
publication_id: str | None
input_hash: str
epoch: int
completed_item_ids: list[str]
validation_id: str | None
pending_action_id: str | None
failure_code: str | None
State schema có version và deserialize validation; không chứa DB connection, tasks, clients, credentials, raw source corpus hoặc unbounded token stream. Fan-out item results dùng keyed merge theo item_id hoặc persist mỗi item rồi reduce IDs; không append list thiếu dedupe lúc replay. Text lớn trong revision/result store, checkpoint chỉ refs.
#8.3 Graphs
Generation/refinement, review và publishing là separate workflow runs có references rõ ràng. Approve không tự publish; cần explicit publish request từ publisher. Review không giữ worker occupied trong thời gian người dùng đọc/sửa. Edit tạo revision mới supersede open review cho old head; new review có workflow/thread mới. Old approval/review history vẫn đọc được.
#8.4 Interrupt/resume protocol
Node await_review gọi interrupt với {review_id,revision_id,approval_digest,allowed_actions}. API nhận reviewer action, authorize và persist decision + workflow command. Worker chỉ resume bằng action_id durable; node sau resume đọc decision từ DB và kiểm tra expected revision/digest/epoch.
LangGraph có thể chạy lại từ đầu node chứa interrupt khi resume; vì vậy phần trước interrupt phải pure hoặc idempotent. Payload phải JSON serializable và không coi resume payload là quyền duyệt. Đây là hành vi được mô tả tại LangGraph interrupts.
#8.5 Worker claim, lease và fencing
Initial defaults: poll 1s, lease 60s, heartbeat 15s; provider timeout không được làm ngừng heartbeat. Dispatcher claim qua short transaction FOR UPDATE SKIP LOCKED, tăng epoch, set lease owner/until. Mỗi commit kết quả kiểm tra workflow epoch và lease owner. Cancel tăng epoch; kết quả đến muộn lưu diagnostic nếu cần nhưng không apply content.
Epoch bảo vệ business commits chưa đủ bảo vệ checkpoint write: worker phải giữ exclusive per-thread advisory lock trên dedicated session khi invoke graph. Nếu mất lock connection, abort invocation và ngừng dùng checkpoint pool; worker mới reconcile trước resume. Production implementation cần test checkpoint saver serialization/fencing dưới connection loss; nếu saver không fence stale writes, dùng wrapper kiểm tra ownership trên cùng checkpoint write transaction. Không gọi recover-safe chỉ vì có lease row.
Graph invocation thất bại không cho worker mới vừa chạy thread cũ vừa tạo thread thay thế. Pin graph_version theo run; deploy giữ worker compatibility cho versions còn waiting_input. Muốn đổi topology/interrupt ordering phải drain hoặc migration checkpoint có fixture, không resume bằng code mới tùy tiện.
#8.6 Crash/recovery matrix
| Crash window | Recovery |
|---|---|
| LLM trả kết quả, chưa persist | Provider reconciliation nếu có; nếu không retry bounded, có thể phát sinh thêm chi phí |
| Persist candidate rồi checkpoint chưa cập nhật | node_executions tìm existing result theo operation_key, không tạo revision mới |
| Approval commit, resume chưa gửi | workflow_commands pending được dispatch lại |
| Resume checkpoint xong, command chưa processed | Reconcile decision/node ledger; không resume interrupt kế tiếp bằng action cũ |
| Cancel trong provider call | Epoch check reject apply/result commit của owner cũ |
| Artifact upload xong, DB finalize chưa commit | Reuse reserved release ID/prefix, verify inventory rồi finalize |
| DB release commit, graph chưa complete | Load release theo revision/publication; return existing result |
| Checkpoint format không tương thích | Mark recovery_required bằng error metadata, giữ workflow failed; chạy worker version cũ/controlled migration |
Retry chỉ transient network/429/5xx; validation/auth/conflict không auto retry. Default tối đa 3 attempts/item, overall deadline 10 phút cho generation và 5 phút publishing; review waiting_input không dùng execution deadline, nhắc việc sau 7 ngày và supersede/close theo policy 30 ngày. Tham số phải configurable, có test fake clock.
#9.1 Conventions
Prefix /api/studio/v1; business routes dưới /sites/{site_id}; tenant từ authenticated principal, nếu user thuộc nhiều tenant phải chọn tenant qua session context đã authorize. Scope URL là site chưa đủ, phải kiểm tra membership cho mỗi ID.
JSON UTF-8; UUID wire lowercase canonical; timestamps RFC3339 UTC; ordinal 1-based; duration integer seconds; unknown request fields reject. Response có request_id header, ETag cho mutable aggregate. PATCH identity chỉ archive/key; nội dung thay bằng create revision. Không PATCH published release.
Mutation current aggregate yêu cầu If-Match: "draft:{uuid}:v{n}" hoặc source equivalent. Thiếu precondition trả 428; stale trả 412. GET historical immutable revision có ETag theo approval_digest. Body có revision_id là explicit input, không server tự dùng latest.
Auth: browser session cookie HttpOnly/Secure/SameSite=Lax, CSRF header cho mutation; service clients bearer token với audience/scopes phù hợp. Same-origin proxy tránh CORS rộng. OIDC login/callback/logout ở auth adapter. Cookie session SSE dùng same-origin EventSource; không nhét bearer token vào URL.
#9.2 Endpoint inventory
Trong bảng, B=/api/studio/v1/sites/{site_id}. Mọi list nhận limit,cursor; client không gửi actor ID. P=publisher, A=author, R=reviewer, C=catalog_admin; read yêu cầu site membership; Runtime reader chỉ release/catalog scope được cấp.
| Method + path | Role | Input / output | Thành công và errors chính |
|---|---|---|---|
GET /health/live | infra | process heartbeat | 200 |
GET /health/ready | infra | DB/schema/pool readiness | 200/503 |
GET /api/studio/v1/me | authenticated | subject, tenants/sites, roles | 200 |
GET /api/studio/v1/sites | authenticated | authorized sites | 200 |
GET B/editor-options | read | locales/audiences/model aliases/policy/defaults | 200 |
GET B/catalog-snapshots | read | snapshot list | 200 |
POST B/catalog-imports | C | versioned snapshot JSON, Idempotency-Key | 202 import workflow; 409 revision digest conflict |
GET B/catalog-imports/{id} | C | import status/issues | 200 |
GET B/catalog-snapshots/{id} | read | metadata + places/targets, paginated children if large | 200 |
GET B/sources | read | filter place_id, locale, archived, query | 200 |
POST B/sources | A | key,title,locale,body,export_policy,source links | 201 source + first revision |
GET B/sources/{id} | read | head metadata/body, ETag | 200 |
POST B/sources/{id}/revisions | A | If-Match; title,body,locale,export_policy,note | 201; 412 |
GET B/sources/{id}/revisions | read | immutable history | 200 |
GET B/source-revisions/{id} | read | exact body/provenance | 200 |
POST B/sources/{id}/archive | A | If-Match | 200; historical pins preserved |
GET B/content-templates | read | locale/audience filters | 200 |
POST B/content-templates | A | template with ordered segments/provenance | 201 |
POST B/content-templates/{id}/revisions | A | If-Match, full content | 201 |
GET B/content-templates/{id}/revisions/{revision_id} | read | exact template | 200 |
GET B/drafts | read | current state + last published, filters | 200 |
POST B/drafts | A | brief + catalog/prompt/policy refs | 201 identity + empty revision |
GET B/drafts/{id} | read | head, ETag, derived flags | 200 |
POST B/drafts/{id}/revisions | A | If-Match, full snapshot, base_revision_id | 201; 412/422 |
GET B/drafts/{id}/revisions | read | branch/history list | 200 |
GET B/draft-revisions/{id} | read | exact complete snapshot | 200 |
GET B/drafts/{id}/diff?from={rev}&to={rev} | read | typed changes by stop/segment/source | 200; both revisions same draft |
POST B/drafts/{id}/archive | A | If-Match | 200 |
POST B/drafts/{id}/duplicate | A | new name/key, exact source_revision_id | 201 new identities |
POST B/drafts/{id}/apply-template | A | If-Match, template_revision_id, stop_id, ordinal, source mapping | 201 new draft revision |
POST B/drafts/{id}/generation-jobs | A | If-Match, base_revision_id, model_alias, scope | 202 job/workflow |
POST B/drafts/{id}/refinement-jobs | A | above + instruction + segment/stop IDs | 202 |
GET B/generation-jobs/{id} | read | progress, result revision, apply_state | 200 |
POST B/generation-jobs/{id}/apply | A | If-Match draft, candidate_revision_id | 200 head; 412 conflict |
POST B/workflows/{id}/cancel | owner/admin | expected epoch | 202; 409 terminal |
POST B/workflows/{id}/retry | owner/admin | failed run, expected epoch | 202; 409 nonretryable |
GET B/workflows/{id} | read | state, pending action, latest sequence | 200 |
GET B/workflows/{id}/events | read | SSE Last-Event-ID | 200 stream; 410 expired cursor |
POST B/draft-revisions/{id}/validations | A/R | policy pinned, validation profile | 202 validation workflow |
GET B/validations/{id} | read | summary, structured issues | 200 |
POST B/draft-revisions/{id}/review-requests | A | If-Match draft, validation_id | 201 review + workflow |
GET B/review-requests | R/A | pending/closed filter | 200 |
GET B/review-requests/{id} | read | exact content refs + actions + pending state | 200 |
POST B/review-requests/{id}/actions | A/R by action | comment/request_changes/withdraw; expected revision/digest | 201 action |
POST B/review-requests/{id}/approvals | R | digest, validation_id, warning IDs, If-Match draft | 201 approval; 409/412/422 |
POST B/approvals/{id}/revocation | R/admin | reason | 201 append-only revocation |
POST B/draft-revisions/{id}/publications | P | approval_id,digest, If-Match draft | 202 publication; 409 not approved |
GET B/publications/{id} | read | stage/status/release_id/failure | 200 |
GET B/releases | read/runtime_reader | finalized only; revoked flag | 200 |
GET B/releases/{id} | read/runtime_reader | immutable metadata + current revocation | 200 |
GET B/releases/{id}/manifest | read/runtime_reader | canonical manifest, ETag | 200/304 |
GET B/releases/{id}/artifact | read/runtime_reader | streamed archive or short-lived signed redirect | 200/302; 409 revoked |
POST B/releases/{id}/verifications | P | request artifact recheck | 202 |
GET B/artifact-verifications/{id} | read | verifier result | 200 |
POST B/releases/{id}/revocation | P | reason | 201 |
GET B/audit-events | site_admin | resource/actor/time filters | 200 |
Catalog import, validation và verification là background operations như mục 5.8. TypeScript/Python enum và DB check phải sinh từ cùng contract inventory. Prompt/policy administration v1 qua reviewed configuration import CLI, không thêm UI admin tùy ý. CLI chạy cùng validation/UoW/auth service identity và audit.
#9.3 Example: Save revision
Các UUID dưới là fixtures minh họa; hashes dùng placeholder, không phải artifact thực.
POST /api/studio/v1/sites/hq/drafts/11111111-1111-4111-8111-111111111111/revisions
If-Match: "draft:11111111-1111-4111-8111-111111111111:v3"
Idempotency-Key: save-draft-tab1-004
Content-Type: application/json
{
"base_revision_id": "22222222-2222-4222-8222-222222222222",
"name": "Đón khách tại HQ",
"locale": "vi",
"audience": {"key": null, "name": "Đối tác", "description": "Đối tác kỹ thuật lần đầu đến văn phòng"},
"target_seconds": 900,
"catalog_snapshot_id": "33333333-3333-4333-8333-333333333333",
"runtime_prompt_revision_id": "44444444-4444-4444-8444-444444444444",
"policy_revision_id": "55555555-5555-4555-8555-555555555555",
"stops": [{
"stop_id": "66666666-6666-4666-8666-666666666666",
"ordinal": 1,
"place_id": "place_reception",
"target_id": "target_reception_presentation",
"title": "Lễ tân",
"source_revision_ids": ["77777777-7777-4777-8777-777777777777"],
"segments": [{
"segment_id": "88888888-8888-4888-8888-888888888888",
"ordinal": 1,
"kind": "welcome",
"text": "Xin chào quý khách đến với VinDynamics.",
"essential": true,
"gesture_cue": {"name": "wave", "when": "before_segment"},
"source_revision_ids": []
}]
}],
"change_reason": "Chỉnh lời chào cho đoàn đối tác"
}
Response 201 có draft_id, revision_id, revision_no, version, content_hash, approval_digest, review_state, has_unpublished_changes; ETag mới ở header. audience.description persist xuyên reload, không chỉ cache trong browser như custom audience flow cũ. Server validate gesture when bằng versioned wire enum before_segment/after_segment; không dùng ba bộ tên enum khác nhau giữa DB/API/artifact.
#9.4 Example: async generation
{
"base_revision_id": "22222222-2222-4222-8222-222222222222",
"model_alias": "studio-default",
"scope": {"type": "stops", "stop_ids": ["66666666-6666-4666-8666-666666666666"]},
"instruction": "Giới thiệu ngắn gọn, giữ nguyên dữ kiện nguồn"
}
{
"job_id": "99999999-9999-4999-8999-999999999999",
"workflow_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"status": "queued",
"base_revision_id": "22222222-2222-4222-8222-222222222222",
"status_url": "/api/studio/v1/sites/hq/generation-jobs/99999999-9999-4999-8999-999999999999"
}
202 Location trỏ status URL; Retry-After gợi ý 2 giây cho polling fallback. Model alias resolve thành provider/model/parameters allowlisted rồi pin vào job; client không được chỉ định arbitrary provider URL hoặc credentials.
#9.5 Error envelope
Content-Type application/problem+json; response không leak SQL/provider stack.
{
"type": "urn:dynostudio:problem:revision-conflict",
"title": "Bản nháp đã được thay đổi",
"status": 412,
"code": "REVISION_CONFLICT",
"detail": "Tải bản mới và so sánh trước khi lưu lại.",
"request_id": "req-example-004",
"current_version": 4,
"current_revision_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"errors": []
}
| Status | Codes |
|---|---|
| 400 | INVALID_CURSOR, MALFORMED_JSON |
| 401/403 | AUTH_REQUIRED, ROLE_REQUIRED, CSRF_FAILED |
| 404 | RESOURCE_NOT_FOUND; dùng cùng response cho inaccessible cross-site ID |
| 409 | APPROVAL_REQUIRED, APPROVAL_REVOKED, REVIEW_SUPERSEDED, IDEMPOTENCY_KEY_REUSED, CATALOG_REVISION_CONFLICT, RELEASE_REVOKED |
| 410 | EVENT_CURSOR_EXPIRED |
| 412/428 | REVISION_CONFLICT / PRECONDITION_REQUIRED |
| 413/422 | PAYLOAD_TOO_LARGE / INVALID_TARGET, INVALID_SOURCE, UNSUPPORTED_LOCALE, VALIDATION_FAILED |
| 429 | GENERATION_QUOTA_EXCEEDED; Retry-After |
| 503 | DATABASE_UNAVAILABLE, STORAGE_UNAVAILABLE; no fake success |
#9.6 SSE và event replay
id: 42
event: generation.item_completed
data: {"workflow_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","item_id":"item-1","completed":1,"total":3}
Event sequence per workflow monotonic, persist trước emit; Last-Event-ID reconnect. Heartbeat 15s, Nginx buffering off. Event retention 7 ngày sau terminal; expired cursor 410, UI GET status snapshot rồi reconnect từ returned latest_sequence. Event payload không có raw prompts/secrets. UI dedupe sequence, status polling fallback khi SSE không dùng được. Không stream LangGraph internal object/schema trực tiếp thành public API.
#10.1 Artifact contract
manifest.json
script.json
sources/<revision-id>.json
prompts/runtime.<locale>.json
Manifest fields bắt buộc: schema_version, canonicalization_version, release_id, tenant_id, site_id, experience_type=office-tour.v1, draft_revision_id, locale, audience snapshot, content_hash, approval_digest, release_digest, catalog provenance, approval metadata, required_capabilities, inventory array.
script.json là ordered stops/segments: semantic IDs, title, text, essential, gesture cues, source revision IDs. V1 mỗi segment là một authored unit; Runtime có thể chia speech chunks nhưng không sửa lời đã duyệt. source payload gồm title/locale/body/revision/content hash và allowed export context. Prompt payload chỉ runtime-approved templates; provider/voice/credentials không đi cùng.
Inventory mỗi item có relative POSIX path, media_type, size_bytes, sha256. manifest.json không nằm trong inventory để tránh self-reference. release_digest = SHA-256 canonical manifest không có field release_digest; source/script/prompt bytes đều được hash trong inventory. approval_digest đã cố định trước release; publication timestamp/actor không làm đổi approval.
Artifact paths cấm absolute path, .., duplicate normalized paths, symlink, backslash ambiguity. Default quotas: 100 stops, 1.000 segments/tour, 20.000 ký tự/segment, 1 MiB/source, 20 MiB JSON artifact và 2.000 files; HTTP revision payload max 5 MiB. Quotas cấu hình, kiểm tra cả archive decompressed bytes. Media files/attachments ngoài JSON scope v1 phải qua schema extension riêng.
#10.2 Publish transaction và storage failure
- API lock draft, verify current revision/approval/validation/permissions; create publication request với reserved release_id + workflow command + idempotency response trong transaction. Nếu existing publication thì trả existing status/resource.
- Worker build payload từ exact pinned revisions; không query source head/latest prompts. Generate immutable artifact tại prefix riêng của publication, dùng conditional create để retry không overwrite bytes khác.
- Upload payloads, manifest cuối; đọc lại/check hash, size, inventory, permissions. Storage version ID được lưu, production bật versioning và quyền write-once theo prefix/object policy nếu provider hỗ trợ.
- Finalize transaction: lock draft, approval và publication theo fixed order; kiểm tra epoch, approval chưa revoked, revision vẫn current theo v1 policy. Draft đã đổi thì publication fail
HEAD_CHANGED, artifact staging có thể GC; explicit publish old revision là feature sau. - Insert sealed script_releases + release_payloads + audit + outbox; set publication succeeded atomically. Release chỉ xuất hiện từ bước này. Sau khi release đã commit, later draft edit không làm mất release cũ.
- Graph retry đọc existing finalized release. Object storage outage trả failure có retry, không tạo status published trước verify.
DB và object store không có distributed transaction. Staging objects chưa referenced là orphan hợp lệ cho GC sau 7 ngày. Objects của release đã finalize tuyệt đối không bị staging GC; GC check DB references/publication lease. Storage mất artifact đã publish là integrity incident: download lỗi, quarantine qua operational state/alert; không sửa immutable release để che lỗi.
#10.3 Runtime boundary
Publish không đồng nghĩa imported/bound/active/ready. Runtime pull artifact trước session, verify authenticated source + hashes + schema + quotas, resolve target với explicit binding revision. Same release_id/different digest phải reject. Missing binding là activation error Runtime; Studio không chặn publication chỉ vì robot offline. Hash chứng minh integrity, không chứng minh publisher identity; service auth/trust configuration là bắt buộc, signing có thể bổ sung khi phân phối offline qua bên thứ ba.
Outbox script.release_published là notification at-least-once, không tự activate robot. Notification delivery failure không undo release. Runtime dedupe bằng event_id/release_id+digest. Raw sources được pin theo stop; shared template không làm context phòng hiện tại bị trộn với phòng khác.
#11.1 Mapping component và hành vi
| Existing component | Work mới |
|---|---|
SetupStep.tsx | Giữ style/form/i18n; persist custom audience description; chọn catalog rõ ràng |
StopsStep.tsx | Giữ selection/reorder; semantic place/target; read-only binding hint |
TeamSourceStep.tsx | Source revisions, per-stop nhiều nguồn, outdated badge và impact preview |
SpeechReviewStep.tsx | Editor + source side-by-side; segment essential/cue; apply suggestion/diff |
SaveTourStep.tsx | Review summary, validation, submit review |
OfflineAudioStep.tsx | Bỏ; thay bằng PublishStep và ReleasePreview |
TourLibrary.tsx | Draft library + release library, published và unpublished changes riêng |
styles.css, i18n/* | Tái sử dụng visual language, spacing, labels VI/EN; bỏ strings audio/runtime không dùng |
Không copy nguyên wizard state logic vào một component lớn. features/studio/{drafts,sources,editor,jobs,review,releases,preview}, shared/api/generated, shared/ui, giữ React Query theo app cũ. API generated từ OpenAPI; fixtures dùng cùng contract để tránh mock shape khác server.
#11.2 Luồng sáu bước
- Thông tin tour: name, locale, audience, estimated target duration, site/catalog; Save draft sớm khi thông tin tối thiểu hợp lệ.
- Điểm dừng: place + target, thứ tự, repeat place với occurrence riêng; không bắt robot online.
- Nguồn nội dung: chọn/pin source revision, sửa source tạo revision mới; explicit áp dụng nguồn mới vào draft, xem tác động tới script.
- Soạn kịch bản: generate/refine, xem job progress, text editing, essential/cue, compare/apply candidate; save status và conflict.
- Kiểm tra và duyệt: complete script + sources, validation issues, warning acknowledgements, reviewer actions trên exact revision; current edit làm review superseded.
- Phát hành: summary đúng bản duyệt, publish status, retry nếu cần, link artifact và preview; badge “Đã phát hành kịch bản”.
Không hiện “Robot ready” hoặc thời lượng WAV. Duration label “Ước lượng phần trình bày”; nếu có walking estimate từ Runtime thì tách nguồn/thời điểm và không nhập vào content approval như measured timing.
#11.3 Routes và state
/studio/sites/:siteId/drafts
/studio/sites/:siteId/drafts/:draftId/edit/:step
/studio/sites/:siteId/drafts/:draftId/revisions/:revisionId
/studio/sites/:siteId/reviews/:reviewId
/studio/sites/:siteId/releases
/studio/sites/:siteId/releases/:releaseId
/studio/sites/:siteId/releases/:releaseId/preview
Server state: React Query key có tenant/site/resource/revision. Local unsaved editor state có base ETag, dirty flag; navigation warning nếu chưa save. On 412 giữ text local, fetch newest và diff, không tự replace textarea. Clear query cache khi logout/tenant change. Refresh khi job chạy fetch status/last sequence rồi reconnect, không POST generate lại.
#11.4 Text preview để test trước
Load từ published artifact đã verify, render stops/segments theo ordinal. FULL đọc tất cả; BRIEF chỉ essential theo policy đã thống nhất; SKIP bỏ stop trong preview khi operator thử, không mutate release. Next/back/repeat/pause điều khiển simulator local, không gọi Runtime API. Gesture hiện cue annotation; timeline có stop/segment/event. Timer nếu dùng chỉ mô phỏng estimate, không chứng minh runtime speech/navigation parity.
MVP preview không dùng TTS. Một fake Runtime importer trong contract tests verify manifest/source/target requirements; không cần xây robot operator UI ở phase Studio. Advanced panel cho QA xem release/digest/validator/source refs. Main author UI hiển thị version, trạng thái duyệt và lỗi có action cụ thể.
#12.1 Deployment profile và config
Dev profile gồm Studio API + worker + PostgreSQL + artifact storage adapter + Web; catalog fixture và fake provider là profile explicit. Production API stateless có thể nhiều replicas; worker concurrency bounded theo tenant/model, checkpointer shared PostgreSQL. Khởi đầu queue trong PostgreSQL, chưa cần Redis/Kafka. Storage production dùng object store với durable versioned objects; filesystem adapter chỉ dev hoặc deployment có persistent volume/backup được test.
| Config | Initial policy |
|---|---|
| STUDIO_DATABASE_DSN | secret; studio_api hoặc studio_worker role; TLS verify-full production |
| STUDIO_CHECKPOINT_DSN | secret riêng, worker only, fixed search_path |
| ARTIFACT_BUCKET / PREFIX | deployment-scoped; access qua scoped keys |
| OIDC_ISSUER / AUDIENCE | fixed allowlist; browser session adapter |
| LLM_PROVIDER_SECRET | secret manager/env injected; không log |
| WORKER_MAX_CONCURRENCY | initial 4 total, 2 active generation/site, tune bằng load test |
| GENERATION_DEADLINE_SECONDS | initial 600; per-item request timeout nhỏ hơn overall |
| IDENTITY/SCHEMA/GRAPH VERSION | pin trong release manifest |
Pool budget ví dụ: 2 API replicas × 10 + 2 workers × 5 business + 2 × 5 checkpoint + advisory-lock sessions + admin headroom phải dưới max_connections. Tính lại khi scale; không dùng mỗi job một pool. Nếu PgBouncer transaction pooling thì advisory session locks/checkpointer requirements cần dedicated session connection hoặc compatible mode, test trước deploy.
Set statement_timeout ban đầu 5s cho API transaction, lock_timeout 2s, idle_in_transaction_session_timeout 15s; migrations/background report có role/timeouts riêng. Mọi durable content commit giữ synchronous_commit=on; fsync/full_page_writes theo safe database defaults. Không tắt durability để tăng benchmark.
#12.2 Migration management
Alembic revision chain + packaged SQL; migrator job giữ migration lock, chạy một lần trước replicas. App startup kiểm tra schema compatibility range, fail readiness nếu thiếu migration. Không auto-upgrade database lúc API boot. Checkpointer package setup/migration chạy bằng checkpoint migrator riêng, pin schema version compatibility cùng graph package.
Expand/contract: thêm nullable column/table/index → backfill bounded batches → validate constraint → switch readers/writers → contract sau rollback window. CREATE INDEX CONCURRENTLY chạy ngoài transaction thích hợp; migration framework phải đánh dấu explicit. Test restore + migrate + previous-binary compatibility; không chỉ downgrade SQL. Migration có checksum và artifact version trong deployment manifest.
#12.3 Security và quyền dữ liệu
Rate limit generation theo actor/site, input size caps và provider budget. Prompt treats source text như data, không instruction/tool authority. Model outputs strict schema, escape khi render browser, không dùng dangerous HTML. Auth kiểm tra download artifact/source permissions, reject cross-site IDs. Service credentials Runtime read-only. Database audit/log không ghi body/prompt theo mặc định; diagnostic trace raw text chỉ opt-in theo environment, hạn retention.
Production database không public internet; TLS; secrets rotation; staging/prod data và buckets tách nhau. Studio cannot assert runtime safety; gesture allowlist là content validation, Runtime vẫn enforce capabilities.
#12.4 Backup, restore và retention
Mục tiêu đề xuất: RPO ≤15 phút, RTO ≤4 giờ cho Studio. Continuous WAL archiving + base backups tạo khả năng PITR; chỉ có pg_dump hằng ngày không đạt RPO đó. PostgreSQL mô tả yêu cầu base backup và chuỗi WAL tại PITR.
Back up database bao gồm workflow checkpoints/ledger và object versions. Backup manifest ghi DB restore point + inventory/storage version references. Restore vào môi trường cô lập: restore DB → verify artifact inventory → disable worker outbound calls → reconcile workflow leases/epochs/checkpoints → enable manual test → cho worker chạy. Không tự resume provider/publish jobs trước reconcile.
Retention initial: events 7 ngày sau terminal; node/provider diagnostic 30 ngày; terminal checkpoints 30 ngày nếu không còn review/recovery dependencies; audit 365 ngày; idempotency 7 ngày; orphan staging 7 ngày. Draft/revision/source được release tham chiếu và finalized artifact giữ theo content-retention policy, không tự purge. Không prune waiting_input checkpoint; archive/close workflow trước. Restore drill ít nhất mỗi quý, ghi thời gian và missing-artifact report.
#12.5 Observability và mục tiêu dịch vụ
Structured logs: request_id, workflow_id, draft/revision/release IDs, actor pseudonymous subject, graph_version, node, attempt, error_code. IDs cardinality cao ở logs/traces, không dùng làm Prometheus label.
Metrics: API latency/error, DB pool saturation, lock waits, queue age, jobs failed/cancelled, provider latency/tokens, checkpoint failures, stale epoch rejected, review waiting age, publication stage duration, artifact mismatch, outbox lag, backup/WAL archive lag.
Initial acceptance targets trong load test: CRUD p95 <500ms với 20 concurrent authors; queue pickup p95 <5s ở workload đã định; text artifact publish p95 <10s cho artifact ≤5MiB khi storage khỏe. Generation latency theo provider/size phải report phân vị riêng, không lấy HTTP 202 latency làm generation speed. Readiness probe DB/schema/pool; provider outage làm generation degraded, không chặn source editing toàn app.
#12.6 Runbooks tối thiểu
| Incident | Action và recovery criterion |
|---|---|
| PostgreSQL down | Readiness fail; reject writes; recover DB, verify migration/leases before worker resume |
| Provider throttling/down | Backoff + quota; fail jobs có code; editor vẫn hoạt động; retry bounded |
| Worker killed/restarted | Reclaim lease + per-thread lock, inspect ledger/checkpoint; no duplicate content/release |
| Storage outage | Publication pending/failed, retry same intent; no published row without verified artifact |
| Artifact mismatch | Block serving corrupted bytes; restore exact object version; verify full inventory |
| Outbox dead letter | Alert + inspect destination; replay same event_id; release remains published |
| Graph upgrade fails resume | Route to compatible worker version; controlled checkpoint migration |
| Disk/WAL retention pressure | Alert early, inspect replication/archive lag, expand/recover; no blind delete WAL |
Không chạy migration trực tiếp vào Office Tour database trong giai đoạn review. Thiết kế v1 dùng database Studio mới và import read-only export đã freeze. Ghi source git SHA, schema migration versions, effective site/catalog authority và source export digest. Không lấy graph DB cũ làm authority nếu deployment đang dùng graph file override.
| Legacy | Destination | Kiểm tra |
|---|---|---|
| documents kind=presentation + revisions | sources/source_revisions | exact body/locale/title/revision mapping |
| documents kind=script/segment + document_beats | materialized draft/template segments | essential/cue/order preserved; strip audio metadata có report |
| refine_suggestions | historical generation provenance + candidate revisions khi mapping đầy đủ | model/prompt/source binding; không giả là authenticated approval mới |
| tour_drafts/tour_draft_stops | drafts/revisions/stops/stop_sources | preserve ordered stops, custom audience nếu export có |
| release_stops/presentation_beats | legacy import candidates / legacy archived artifact reference | use immutable release content; không reconstruct từ current documents |
| POI checkpoint mapping | catalog snapshot + migration mapping | target semantic; binding thuộc Runtime |
| bundle_files/WAV | ở hệ thống legacy | không import vào Studio schema |
Nếu history cũ thiếu title/cue/source tại revision, ghi provenance_incomplete trong report và bắt human review trước publication mới. Không bịa lịch sử hoặc reuse human_approved boolean như signed reviewer action. Source reading bug nguy hiểm: lấy documents.body current khi draft pin source revision cũ; importer phải resolve document_revisions exact và report missing rows.
Các bước: export snapshot → dry-run mapping/report → import isolated DB → differential compare text/order/essential/cues → manually review migrated sample → publish new contract release → fake Runtime verify → cut over Studio WebUI/API. Import batch idempotent, A→B→A và duplicate source bytes vẫn giữ revision history.
Rollback Studio bằng image/graph version + compatible DB schema + artifact versions; old Office Tour stack giữ nguyên trong cửa sổ rollback. Không dual-write editor vào hai DB. Export các edits mới trước rollback nếu cần reconciliation; chạy lại app cũ không tự hiểu new Studio revisions. GitLab repo setup/deploy/hardware migration ngoài deliverable Markdown này.
| Phase | Việc làm cụ thể | Deliverable / gate |
|---|---|---|
| P0 Contract freeze | Review schema/hash/API examples; pin stack; create 3-stop VI + EN + custom audience fixtures; freeze legacy baseline | OpenAPI/JSON Schema/golden fixtures + ADR decisions; không schema contradiction |
| P1 PostgreSQL foundation | Alembic, scoped FK, seal triggers, roles/RLS, repository/UoW, idempotency/audit; dedicated checkpointer schema | Migrations run fresh/upgrade; negative SQL tests; clean wheel integration |
| P2 Manual authoring slice | Source/catalog import; drafts/segments/revisions/diff; port Setup/Stops/Source/editor UI; save/reload/conflict | Tạo tour bằng text thủ công, lưu/reload toàn bộ, chưa cần provider |
| P3 LangGraph generation | Job scheduling, pin inputs, provider/fake adapters, durable checkpoint, bounded fanout, result apply, events/cancel | Restart/replay/late result tests; UI generate/refine/apply hoạt động |
| P4 Human review + publish | Validation, review graph interrupt/resume, role approval, staging/finalization graph, hashes/inventory | Exact approval; duplicate publish; revoke/finalize race; storage crash tests |
| P5 Web test feature | Complete wizard, libraries, release artifact viewer, FULL/BRIEF text preview, Playwright | End-to-end từ source đến verified published preview không có TTS credentials |
| P6 Migration + contract | Legacy importer/report, fake Runtime importer, catalog remap fixtures | Text/order/cue parity; missing binding separated from content validity |
| P7 Production readiness | OIDC/RLS negative tests, TLS/proxy/SSE, load test, backup restore, runbooks, CI/deploy pins | Measured SLO report, restore drill, approved production checklist |
Dependency order: P0 → P1 → P2 → P3 → P4; UI được dùng từ P2 để test sớm, hoàn thiện P5. P6/P7 chỉ gate sau khi vertical slice thật đã chạy. Không cần đợi migrate Runtime/audio/navigation mới kiểm thử Studio.
Trong mỗi phase, code phải có unit/contract/integration tương ứng, không dồn toàn bộ verification tới cuối. SQL reference trong file này chuyển thành migrations có thứ tự prerequisites, CHECK enum đầy đủ, FK/index và triggers thực thi; dictionary không thay executable schema.
| Nhóm | Scenarios bắt buộc | Pass condition |
|---|---|---|
| Scope/DB | Same UUID khác tenant; cross-site source/target; wrong current pointer; parent revision từ draft khác | FK/RLS/API reject phù hợp, không chỉ UI hide |
| Revision | Revert A→B→A; same target ở 2 stops; append child vào sealed parent; concurrent seal/insert | Preserve history/repeated visit; immutable rows reject mutation |
| Source | Nhiều nguồn/segment; source head đổi; archived source đã pin; export denied | Đọc exact source, no silent upgrade/export |
| Editor | Refresh custom audience; unsaved text khi 412; order persistence; locale switch | Không mất dữ liệu hoặc approve bản khác |
| LLM | Timeout/429; partial stop failure; malformed output; prompt injection in source | Bounded retry, structured failure, no unauthorized tool/content apply |
| Concurrency | 2 tabs save; approve vs edit; revoke vs finalize; double publish | One valid current update; stale approval rejected; one release/revision |
| LangGraph | Restart before/after interrupt; duplicate resume; DB commit before checkpoint; lost advisory connection | Resume đúng action/thread, no duplicate candidate/release/checkpoint corruption |
| Cancellation | Cancel rồi LLM response; lease expires; stale worker persists result | Epoch fencing rejects stale apply |
| Artifact | Alter source bytes; missing file; duplicate path; zip bomb; same ID different hash | Verify reject; no finalized invalid release |
| Hash | Python/TS golden fixtures; Unicode/LF; remap only; text/cue/order/source changed | Deterministic; semantic/provenance hashes đổi đúng lớp |
| Runtime contract | No binding; unsupported schema/capability; published release khác active session | Import/activation boundaries giữ đúng; no Studio dependency per turn |
| Operations | Fresh install, expand migration, DB restore + object versions, SSE cursor expiry | Recover without data loss beyond measured RPO, explicit degraded state |
| Web E2E | Manual authoring → generate/apply → review → publish → reload preview | UI works against real API/PG plus fake provider, không chỉ mocks |
Definition of done: PostgreSQL migrations/constraints chạy thật; LangGraph compiled workflows + PostgreSQL checkpoint resume được; WebUI authoring/publish/preview hoạt động; artifact chỉ script/source/prompts; no offline audio dependencies; exact reviewer approval; production auth/scoping; regression + crash tests; restore drill và runbook. Scaffold imports/build smoke chưa đủ điều kiện này.
Local design: 04 — data/script, 07 — Python architecture, 08 — Studio/Runtime LangGraph. Reference implementation: Office Tour wizard, draft storage, legacy SQL.
Các lựa chọn implementation/schema/API/SLO trong tài liệu là đề xuất của project. Tài liệu chính thức liên kết tại các mục tương ứng chỉ hỗ trợ behavior framework/database, không xác nhận kiến trúc này đã production-ready.
Các điểm review trước P0: chấp thuận PostgreSQL 17 baseline; explicit Apply suggestion; một locale/revision; source export policy v1; reviewer khác last editor ở production; publish chỉ current approved revision; durations 5–60 phút theo baseline; preview text không audio; snapshot provenance tách semantic content hash. Có thể điều chỉnh những policy này trước implementation, nhưng phải sửa đồng bộ DB/API/UI/fixtures.