DynoStudio — Thiết kế production, PostgreSQL, LangGraph, API và WebUI

publicv1
29d ago
4 views0 comments0 reviews46 min read
raw .md ↗

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.

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ồnHiệ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.tsxShell 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.pyCó expected revision, nhưng draft stops bị thay wholesale; đọc source từ current document
Office Tour storage/migrations/001_initial.sql, 004_tour_drafts.sqlRelease gắn checkpoint/map và render status; cần schema Studio mới
Office Tour document_beats, tour_stop_segmentsCần giữ essential, cue, ordered composition và shared content
architecture/04, 07, 08Ownership 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ướcQuyế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 sourceQuan hệ nhiều-nhiều segment_sources, FK tới source đã pin cho stop
Dedupe mọi job bằng input hashInput hash để provenance/cache; dedupe request bằng idempotency key. Explicit regenerate được tạo job mới
Snapshot catalog digest nằm trong content hashBinding/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 siteMột release trên revision; tour khác có thể dùng cùng nội dung
UNIQUE payload theo SHABỏ; hai path hợp lệ có thể chứa bytes giống nhau
Approval vừa immutable vừa có trường revoke mutableApproval 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 releaseDùng publication intent, staging/verify artifact, rồi transaction finalize release; chỉ finalized release được liệt kê public
UUID PK đủ chống sai siteDù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ự độngGrounding 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ìnhChi 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

HashNội dungMục đích
content_hashOrdered semantic script, locale/audience/brief, semantic place/target IDs, source content hashes, approved runtime prompts, cues, essential, schema semantics versionNhận diện nội dung thực tế
approval_digestcontent_hash + exact revision ID + source revision IDs + catalog snapshot identity/digest + policy/prompt pack revisionsReviewer duyệt đúng bản và provenance
release_digestCanonical manifest body với payload inventory, approval metadata và release identity; loại chính trường release_digestRuntime 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

  1. Mọi FK nghiệp vụ cùng tenant/site; UUID không thay authorization.
  2. Current pointer thuộc đúng source/draft identity; parent revision thuộc cùng draft.
  3. Target thuộc đúng place và catalog snapshot draft đang pin.
  4. Source lineage từ segment chỉ tham chiếu source đã pin cho stop, cùng scope.
  5. Revision và child rows được seal trong cùng transaction; sau seal cấm INSERT/UPDATE/DELETE children.
  6. Review/approval/publish chỉ dùng revision sealed.
  7. Approval gắn revision, approval_digest, validation run, policy version, actor từ auth.
  8. Revoked approval không authorize publication mới; release đã phát hành có lifecycle revocation riêng.
  9. Một publication intent cho một draft revision; retry reuse intent/release ID.
  10. Published release và payload inventory immutable; artifact tồn tại/verify trước khi finalization.
  11. Job result dựa vào revision cũ không tự ghi đè current revision mới.
  12. 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

TableColumnsConstraints
tenantstenant_id text; name nonblank; disabled_at timestamptz?; created_atPK tenant_id
sitesS; name nonblank; default_locale text; archived_at timestamptz?; created_atPK S; FK tenant_id
site_membershipsS; 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

TableColumnsConstraints
catalog_snapshotsS; 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_placesS; snapshot_id; place_id text; stable_code text; title_i18n jsonb; semantic_description_i18n jsonb; retired booleanPK(S,snapshot_id,place_id); FK snapshot; UQ(S,snapshot_id,stable_code)
catalog_targetsS; 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

TableColumnsConstraints
sourcesS; source_id; source_key nonblank; current_revision_id?; version bigint default 0; archived_at?; created_at/byPK(S,source_id); UQ(S,source_key); scoped owner-aware current FK
source_revisionsS; source_id; revision_id; revision_no int; title nonblank; locale text; body nonblank; content_hash sha256; export_policy text; note text; created_at/byPK(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_linksS; source_id; catalog_snapshot_id; place_idPK(S,source_id,catalog_snapshot_id,place_id); FK source and catalog place
content_templatesS; template_id; template_key; current_revision_id?; version bigint; archived_at?PK(S,template_id); UQ(S,template_key)
content_template_revisionsS; template_id; revision_id; revision_no; title; locale; audience_description; sealed_at?; created_at/byPK(S,revision_id); UQ(S,template_id,revision_id); UQ(S,template_id,revision_no); FK template
template_segmentsS; 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

TableColumnsConstraints
prompt_pack_revisionsS; prompt_revision_id; pack_key; version; purpose generation/runtime; locale; templates jsonb; digest sha256; created_at/byPK(S,prompt_revision_id); UQ(S,pack_key,version)
policy_revisionsS; policy_revision_id; policy_key; version; schema_version; rules jsonb; digest sha256; created_at/byPK(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

TableColumnsConstraints
validation_runsS; validation_id; revision_id; approval_digest; validator_version; estimator_version; policy_revision_id; status passed/failed; estimated_seconds int; summary jsonb; created_atPK(S,validation_id); UQ(S,revision_id,approval_digest,validation_id); FK exact revision/digest, policy
validation_issuesS; validation_id; issue_id; severity error/warning/info; code; scope_path text; message; detail jsonbPK(S,validation_id,issue_id); FK validation; scope_path JSON pointer into canonical revision
review_requestsS; review_id; revision_id; validation_id; approval_digest; workflow_id; created_at/byPK(S,review_id); UQ(S,review_id,revision_id,approval_digest,validation_id); FK validation tuple
review_actionsS; action_id; review_id; kind comment/request_changes/approve/withdraw; scope_path?; comment; acknowledged_issue_ids jsonb; created_at/byPK(S,action_id); FK review; append-only
content_approvalsS; approval_id; review_id; revision_id; approval_digest; validation_id; action_id; reviewer_id; created_atPK(S,approval_id); UQ(S,approval_id,revision_id,approval_digest); FK exact review tuple; FK action; application verifies action kind/actor
approval_revocationsS; approval_id; reason nonblank; created_at/byPK(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

TableColumnsConstraints
workflow_runsS; 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_commandsS; 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_jobsS; 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_atPK(S,job_id); UQ(S,workflow_id); FK workflow/prompts/revisions; no unique input_hash
generation_itemsS; 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_executionsS; 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_eventsS; workflow_id; sequence bigint; type; payload jsonb; created_atPK(S,workflow_id,sequence); FK workflow; sequence assigned under workflow row lock
background_operationsS; operation_id; workflow_id; kind catalog_import/validate/verify_artifact; input_ref text; input_hash; result_id uuid?; created_at/byPK(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

TableColumnsConstraints
publication_requestsS; 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/byPK(S,publication_id); UQ(S,revision_id); UQ(S,reserved_release_id); FK workflow, owned draft revision, exact approval tuple
script_releasesS; 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_byPK(S,release_id); UQ(S,revision_id); UQ(S,draft_id,release_no); UQ(S,publication_id); FK publication/approval/revision
release_payloadsS; release_id; path; sha256; size_bytes bigint; media_type; storage_refPK(S,release_id,path); FK release; size_bytes>=0; no unique sha256
release_revocationsS; release_id; reason; created_at/byPK(S,release_id); FK release; append-only
artifact_verificationsS; verification_id; release_id; verifier_version; status passed/failed; issues jsonb; created_atPK(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

TableColumnsConstraints
idempotency_keysS; actor_id; operation; key; request_hash; status in_progress/completed; response_status?; response_body?; resource_id?; created_at; expires_atPK(S,actor_id,operation,key); response body bounded/redacted
outbox_eventsS; event_id; aggregate_kind; aggregate_id; type; schema_version; payload jsonb; status pending/claimed/delivered/dead_letter; attempts; next_attempt_at; lease_until?; created_atPK(S,event_id); event_id downstream dedupe
audit_eventsS; audit_id uuid; actor_id; action; resource_kind; resource_id; revision_id?; request_id; detail jsonb; created_atPK(S,audit_id); append-only; text/secrets excluded by default
migration_importsS; batch_id; legacy_kind; legacy_id; legacy_revision text; source_digest; destination_id; result jsonb; created_atPK(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

QueryIndex bổ sung ngoài PK/UNIQUE
Draft librarydrafts(S,updated_at DESC,draft_id DESC) WHERE archived_at IS NULL
Revision historydraft_revisions(S,draft_id,revision_no DESC)
Source historysource_revisions(S,source_id,revision_no DESC)
Source impactstop_sources(S,source_revision_id,revision_id)
Review queuereview_requests(S,created_at DESC,review_id DESC), approvals(S,revision_id)
Release libraryscript_releases(S,published_at DESC,release_id DESC)
Job queueworkflow_runs(status,lease_until,created_at) WHERE status IN ('queued','running')
Pending commandsworkflow_commands(status,created_at) WHERE status='pending'
Outboxoutbox_events(status,next_attempt_at) WHERE status IN ('pending','claimed')
Auditaudit_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

  1. BEGIN READ COMMITTED; set tenant/site context.
  2. Lock draft FOR UPDATE, kiểm tra archive, role và expected version.
  3. Allocate revision_no dưới lock; validate request shape, catalog/source/policy links.
  4. Insert revision unsealed + stops/segments/source links. Tính lại canonical hashes ở server.
  5. Validate topology; seal. Cập nhật current_revision_id và version+1.
  6. 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 stateAuthorityVí dụ
Nội dung và quyền duyệtStudio relational tablesexact revision, approval, release
Execution positionLangGraph checkpointnext node, pending interrupt, retry state
Scheduling/recoveryworkflow_runs/commands/executionslease, epoch, result refs, command dedupe
Browser stateReact local stateunsaved 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 windowRecovery
LLM trả kết quả, chưa persistProvider 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ậtnode_executions tìm existing result theo operation_key, không tạo revision mới
Approval commit, resume chưa gửiworkflow_commands pending được dispatch lại
Resume checkpoint xong, command chưa processedReconcile decision/node ledger; không resume interrupt kế tiếp bằng action cũ
Cancel trong provider callEpoch check reject apply/result commit của owner cũ
Artifact upload xong, DB finalize chưa commitReuse reserved release ID/prefix, verify inventory rồi finalize
DB release commit, graph chưa completeLoad release theo revision/publication; return existing result
Checkpoint format không tương thíchMark 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 + pathRoleInput / outputThành công và errors chính
GET /health/liveinfraprocess heartbeat200
GET /health/readyinfraDB/schema/pool readiness200/503
GET /api/studio/v1/meauthenticatedsubject, tenants/sites, roles200
GET /api/studio/v1/sitesauthenticatedauthorized sites200
GET B/editor-optionsreadlocales/audiences/model aliases/policy/defaults200
GET B/catalog-snapshotsreadsnapshot list200
POST B/catalog-importsCversioned snapshot JSON, Idempotency-Key202 import workflow; 409 revision digest conflict
GET B/catalog-imports/{id}Cimport status/issues200
GET B/catalog-snapshots/{id}readmetadata + places/targets, paginated children if large200
GET B/sourcesreadfilter place_id, locale, archived, query200
POST B/sourcesAkey,title,locale,body,export_policy,source links201 source + first revision
GET B/sources/{id}readhead metadata/body, ETag200
POST B/sources/{id}/revisionsAIf-Match; title,body,locale,export_policy,note201; 412
GET B/sources/{id}/revisionsreadimmutable history200
GET B/source-revisions/{id}readexact body/provenance200
POST B/sources/{id}/archiveAIf-Match200; historical pins preserved
GET B/content-templatesreadlocale/audience filters200
POST B/content-templatesAtemplate with ordered segments/provenance201
POST B/content-templates/{id}/revisionsAIf-Match, full content201
GET B/content-templates/{id}/revisions/{revision_id}readexact template200
GET B/draftsreadcurrent state + last published, filters200
POST B/draftsAbrief + catalog/prompt/policy refs201 identity + empty revision
GET B/drafts/{id}readhead, ETag, derived flags200
POST B/drafts/{id}/revisionsAIf-Match, full snapshot, base_revision_id201; 412/422
GET B/drafts/{id}/revisionsreadbranch/history list200
GET B/draft-revisions/{id}readexact complete snapshot200
GET B/drafts/{id}/diff?from={rev}&to={rev}readtyped changes by stop/segment/source200; both revisions same draft
POST B/drafts/{id}/archiveAIf-Match200
POST B/drafts/{id}/duplicateAnew name/key, exact source_revision_id201 new identities
POST B/drafts/{id}/apply-templateAIf-Match, template_revision_id, stop_id, ordinal, source mapping201 new draft revision
POST B/drafts/{id}/generation-jobsAIf-Match, base_revision_id, model_alias, scope202 job/workflow
POST B/drafts/{id}/refinement-jobsAabove + instruction + segment/stop IDs202
GET B/generation-jobs/{id}readprogress, result revision, apply_state200
POST B/generation-jobs/{id}/applyAIf-Match draft, candidate_revision_id200 head; 412 conflict
POST B/workflows/{id}/cancelowner/adminexpected epoch202; 409 terminal
POST B/workflows/{id}/retryowner/adminfailed run, expected epoch202; 409 nonretryable
GET B/workflows/{id}readstate, pending action, latest sequence200
GET B/workflows/{id}/eventsreadSSE Last-Event-ID200 stream; 410 expired cursor
POST B/draft-revisions/{id}/validationsA/Rpolicy pinned, validation profile202 validation workflow
GET B/validations/{id}readsummary, structured issues200
POST B/draft-revisions/{id}/review-requestsAIf-Match draft, validation_id201 review + workflow
GET B/review-requestsR/Apending/closed filter200
GET B/review-requests/{id}readexact content refs + actions + pending state200
POST B/review-requests/{id}/actionsA/R by actioncomment/request_changes/withdraw; expected revision/digest201 action
POST B/review-requests/{id}/approvalsRdigest, validation_id, warning IDs, If-Match draft201 approval; 409/412/422
POST B/approvals/{id}/revocationR/adminreason201 append-only revocation
POST B/draft-revisions/{id}/publicationsPapproval_id,digest, If-Match draft202 publication; 409 not approved
GET B/publications/{id}readstage/status/release_id/failure200
GET B/releasesread/runtime_readerfinalized only; revoked flag200
GET B/releases/{id}read/runtime_readerimmutable metadata + current revocation200
GET B/releases/{id}/manifestread/runtime_readercanonical manifest, ETag200/304
GET B/releases/{id}/artifactread/runtime_readerstreamed archive or short-lived signed redirect200/302; 409 revoked
POST B/releases/{id}/verificationsPrequest artifact recheck202
GET B/artifact-verifications/{id}readverifier result200
POST B/releases/{id}/revocationPreason201
GET B/audit-eventssite_adminresource/actor/time filters200

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": []
}
StatusCodes
400INVALID_CURSOR, MALFORMED_JSON
401/403AUTH_REQUIRED, ROLE_REQUIRED, CSRF_FAILED
404RESOURCE_NOT_FOUND; dùng cùng response cho inaccessible cross-site ID
409APPROVAL_REQUIRED, APPROVAL_REVOKED, REVIEW_SUPERSEDED, IDEMPOTENCY_KEY_REUSED, CATALOG_REVISION_CONFLICT, RELEASE_REVOKED
410EVENT_CURSOR_EXPIRED
412/428REVISION_CONFLICT / PRECONDITION_REQUIRED
413/422PAYLOAD_TOO_LARGE / INVALID_TARGET, INVALID_SOURCE, UNSUPPORTED_LOCALE, VALIDATION_FAILED
429GENERATION_QUOTA_EXCEEDED; Retry-After
503DATABASE_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

  1. 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.
  2. 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.
  3. 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ợ.
  4. 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.
  5. 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ũ.
  6. 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 componentWork mới
SetupStep.tsxGiữ style/form/i18n; persist custom audience description; chọn catalog rõ ràng
StopsStep.tsxGiữ selection/reorder; semantic place/target; read-only binding hint
TeamSourceStep.tsxSource revisions, per-stop nhiều nguồn, outdated badge và impact preview
SpeechReviewStep.tsxEditor + source side-by-side; segment essential/cue; apply suggestion/diff
SaveTourStep.tsxReview summary, validation, submit review
OfflineAudioStep.tsxBỏ; thay bằng PublishStep và ReleasePreview
TourLibrary.tsxDraft 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

  1. 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ệ.
  2. Điểm dừng: place + target, thứ tự, repeat place với occurrence riêng; không bắt robot online.
  3. 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.
  4. Soạn kịch bản: generate/refine, xem job progress, text editing, essential/cue, compare/apply candidate; save status và conflict.
  5. 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.
  6. 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.

ConfigInitial policy
STUDIO_DATABASE_DSNsecret; studio_api hoặc studio_worker role; TLS verify-full production
STUDIO_CHECKPOINT_DSNsecret riêng, worker only, fixed search_path
ARTIFACT_BUCKET / PREFIXdeployment-scoped; access qua scoped keys
OIDC_ISSUER / AUDIENCEfixed allowlist; browser session adapter
LLM_PROVIDER_SECRETsecret manager/env injected; không log
WORKER_MAX_CONCURRENCYinitial 4 total, 2 active generation/site, tune bằng load test
GENERATION_DEADLINE_SECONDSinitial 600; per-item request timeout nhỏ hơn overall
IDENTITY/SCHEMA/GRAPH VERSIONpin 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

IncidentAction và recovery criterion
PostgreSQL downReadiness fail; reject writes; recover DB, verify migration/leases before worker resume
Provider throttling/downBackoff + quota; fail jobs có code; editor vẫn hoạt động; retry bounded
Worker killed/restartedReclaim lease + per-thread lock, inspect ledger/checkpoint; no duplicate content/release
Storage outagePublication pending/failed, retry same intent; no published row without verified artifact
Artifact mismatchBlock serving corrupted bytes; restore exact object version; verify full inventory
Outbox dead letterAlert + inspect destination; replay same event_id; release remains published
Graph upgrade fails resumeRoute to compatible worker version; controlled checkpoint migration
Disk/WAL retention pressureAlert 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.

LegacyDestinationKiểm tra
documents kind=presentation + revisionssources/source_revisionsexact body/locale/title/revision mapping
documents kind=script/segment + document_beatsmaterialized draft/template segmentsessential/cue/order preserved; strip audio metadata có report
refine_suggestionshistorical generation provenance + candidate revisions khi mapping đầy đủmodel/prompt/source binding; không giả là authenticated approval mới
tour_drafts/tour_draft_stopsdrafts/revisions/stops/stop_sourcespreserve ordered stops, custom audience nếu export có
release_stops/presentation_beatslegacy import candidates / legacy archived artifact referenceuse immutable release content; không reconstruct từ current documents
POI checkpoint mappingcatalog snapshot + migration mappingtarget semantic; binding thuộc Runtime
bundle_files/WAVở hệ thống legacykhô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.

PhaseViệc làm cụ thểDeliverable / gate
P0 Contract freezeReview schema/hash/API examples; pin stack; create 3-stop VI + EN + custom audience fixtures; freeze legacy baselineOpenAPI/JSON Schema/golden fixtures + ADR decisions; không schema contradiction
P1 PostgreSQL foundationAlembic, scoped FK, seal triggers, roles/RLS, repository/UoW, idempotency/audit; dedicated checkpointer schemaMigrations run fresh/upgrade; negative SQL tests; clean wheel integration
P2 Manual authoring sliceSource/catalog import; drafts/segments/revisions/diff; port Setup/Stops/Source/editor UI; save/reload/conflictTạo tour bằng text thủ công, lưu/reload toàn bộ, chưa cần provider
P3 LangGraph generationJob scheduling, pin inputs, provider/fake adapters, durable checkpoint, bounded fanout, result apply, events/cancelRestart/replay/late result tests; UI generate/refine/apply hoạt động
P4 Human review + publishValidation, review graph interrupt/resume, role approval, staging/finalization graph, hashes/inventoryExact approval; duplicate publish; revoke/finalize race; storage crash tests
P5 Web test featureComplete wizard, libraries, release artifact viewer, FULL/BRIEF text preview, PlaywrightEnd-to-end từ source đến verified published preview không có TTS credentials
P6 Migration + contractLegacy importer/report, fake Runtime importer, catalog remap fixturesText/order/cue parity; missing binding separated from content validity
P7 Production readinessOIDC/RLS negative tests, TLS/proxy/SSE, load test, backup restore, runbooks, CI/deploy pinsMeasured 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ómScenarios bắt buộcPass condition
Scope/DBSame UUID khác tenant; cross-site source/target; wrong current pointer; parent revision từ draft khácFK/RLS/API reject phù hợp, không chỉ UI hide
RevisionRevert A→B→A; same target ở 2 stops; append child vào sealed parent; concurrent seal/insertPreserve history/repeated visit; immutable rows reject mutation
SourceNhiều nguồn/segment; source head đổi; archived source đã pin; export deniedĐọc exact source, no silent upgrade/export
EditorRefresh custom audience; unsaved text khi 412; order persistence; locale switchKhông mất dữ liệu hoặc approve bản khác
LLMTimeout/429; partial stop failure; malformed output; prompt injection in sourceBounded retry, structured failure, no unauthorized tool/content apply
Concurrency2 tabs save; approve vs edit; revoke vs finalize; double publishOne valid current update; stale approval rejected; one release/revision
LangGraphRestart before/after interrupt; duplicate resume; DB commit before checkpoint; lost advisory connectionResume đúng action/thread, no duplicate candidate/release/checkpoint corruption
CancellationCancel rồi LLM response; lease expires; stale worker persists resultEpoch fencing rejects stale apply
ArtifactAlter source bytes; missing file; duplicate path; zip bomb; same ID different hashVerify reject; no finalized invalid release
HashPython/TS golden fixtures; Unicode/LF; remap only; text/cue/order/source changedDeterministic; semantic/provenance hashes đổi đúng lớp
Runtime contractNo binding; unsupported schema/capability; published release khác active sessionImport/activation boundaries giữ đúng; no Studio dependency per turn
OperationsFresh install, expand migration, DB restore + object versions, SSE cursor expiryRecover without data loss beyond measured RPO, explicit degraded state
Web E2EManual authoring → generate/apply → review → publish → reload previewUI 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.

comments (0)

reviews (0)