Skip to content

文獻收斂為第三個實體聚合:TEXT_CODES+TEXT_INSTANCE_DATA 版本層級 - #1267

Merged
sudoghut merged 1 commit into
cbdb-project:developfrom
frankslin:develop
Aug 22, 2026
Merged

文獻收斂為第三個實體聚合:TEXT_CODES+TEXT_INSTANCE_DATA 版本層級#1267
sudoghut merged 1 commit into
cbdb-project:developfrom
frankslin:develop

Conversation

@frankslin

Copy link
Copy Markdown
Contributor
  • 新增聚合根 TextImportService:把批次匯入書稿的存儲過程(c_textid 配號、書名正規化與字形標準化、拼音派生、稽核蓋章)抽成單一真源, AdminBatchLoadBookTitlesController 改為委派、只留批次語義
  • 文獻的兩重層級分開處理:TEXT_INSTANCE_DATA 版本列屬聚合內部, update 以 (edition_id, instance_id) 為列鍵做集合對賬、delete 先子後父; c_source 自引用(著錄來源樹)為跨實體引用,update 有成環護欄 (422 source_cycle)、delete 引用計數含子文獻與其他文獻版本列
  • 實體級 mutation API:resource=text-entity(別名 book/books;不用 text——那是人物著述子資源 BIOG_TEXT_DATA 的既有別名),經通用 EntityAggregate*Handler 分派,direct 與 proposal 皆可用
  • 專屬前端頁 /app/text(Index/Create/Edit+版本列編輯器),列表走 EntityTableBrowser、計算欄 instance_count/child_count;側欄 「文獻代碼表」改指實體頁;新增 text_entity i18n(zh-TW/en)
  • 裸表寫入收斂拆兩半:text-codes 裸表 create/delete 下架(改回 501, 聚合 API 完整取代);codes UI 封寫暫緩待 parity(作者列表面板、 instance 專屬欄位),補齊後加入 closed_code_tables 即自動封寫
  • 同步 API.md §4/§13、ENTITY_AGGREGATE_ARCHITECTURE.md、CHANGELOG
  • 已執行 ./vendor/bin/phpunit 全套(既有 SecurityAuditLogTest 環境性 失敗除外全綠)、npm run build、php-cs-fixer

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba37022ca0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +127 to +128
if ($editionId === null || $editionId === '' || !ctype_digit((string) $editionId)
|| $instanceId === null || $instanceId === '' || !ctype_digit((string) $instanceId)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject zero-valued version keys

When an API caller or the new form sends edition_id or instance_id as 0, ctype_digit() accepts it even though these keys are documented here as positive integers. The normalized input then inserts or reconciles TEXT_INSTANCE_DATA rows with c_text_edition_id/c_text_instance_id = 0, creating invalid aggregate keys; parse the values and require them to be greater than zero before accepting the row.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修,但沒有照建議無條件拒絕 0——先查了生產庫,那樣會造成比原問題更嚴重的退化。

TEXT_INSTANCE_DATA 共 9,817 列,其中確實存在一列 0 值鍵

c_textid c_text_edition_id c_text_instance_id c_instance_title_chn
40354 0 0 順義縣志

其餘 9,816 列的兩個鍵都 ≥ 1。若在 validate() 裡無條件擋下 0,/app/text/40354/edit完全無法儲存:編輯頁載入聚合時會把既有版本列原樣帶進表單、送出時原樣回送,於是使用者連一個字都沒碰那列也會吃 422。那是把「防止產生新的無效鍵」換成「讓一筆既有文獻永久唯讀」。

所以改成分兩層,只擋真正會新產生無效鍵的情況:

  • createResolvesTextAggregateInput):一律拒絕,新建文獻不可能有既有列 → 422 instances.N.key: positive_integer_required
  • updateTextAggregateDefinition::guardWrite()):比對 $existing['instances'],只放行資料庫裡本來就是該 0 值鍵的那一列;任何資料庫中不存在的 0 值鍵一樣 422。

放在 guardWrite() 而不是 validate(),是因為 validate(string $operation, array $changes) 契約上拿不到 target.pk 與既有聚合,而 guardWrite($operation, $id, $input, $existing) 兩者都有。

回歸測試(ApiV2MutateTextEntityTest):

  • testCreateRejectsZeroVersionKeysedition_id=0instance_id=0 兩種各自 422,且不落庫。
  • testUpdateRejectsNewZeroVersionKeyButKeepsExistingLegacyRow:既有 0/0 列原樣回送 → 200;新增一個庫中不存在的 0 值鍵 → 422。

Comment on lines +351 to +352
{instances.map((r, i) => (
<div key={i} className="rounded-md border border-border p-3">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use stable keys for version rows

For the editable instances list, using the array index means that removing a row causes React to reuse the DOM for a different (edition_id, instance_id) row, so browser state such as focus or validation can be associated with the wrong version entry. These rows already have a composite key, so key them by that stable identity (with a client-side temporary id for unsaved/blank rows).

AGENTS.md reference: AGENTS.md:L57-L57

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修,感謝——這條確實違反了 AGENTS.md §4「React 列表不要使用 index 當 key」。

實作上採「純前端穩定 id」,而不是建議的複合鍵:

interface InstanceRowState extends InstanceRow {
    _uid: string;
}
let instanceUidSeq = 0;
const nextInstanceUid = () => `inst-${++instanceUidSeq}`;

初始化時替既有列各配一個 uid,addInst() 產生新 uid;setInst()removeInst() 的定位也一併從索引改成 uid(不只 key 屬性——用索引定位的更新在刪列後同樣會寫到錯的列上)。

沒有用 (edition_id, instance_id) 當 key 的原因:這兩個欄位在這張表單裡是使用者可編輯的輸入框。拿它們當 key 的話,使用者在「版本組號」欄打字時每按一鍵都會換掉 key → 整列 unmount/remount → 焦點與游標位置立刻丟失,反而製造一個比原問題更明顯的體感 bug。純前端 uid 在列的整個生命週期內恆定,既有列與未存檔的新列都適用,不需要另外區分 temp id。

送出時不受影響:payload 是逐欄明列組出來的,_uid 不會外洩到 API。

@sudoghut

Copy link
Copy Markdown
Member

"This branch has conflicts that must be resolved" 这个 issue 拜托 Frank 啦

- 新增聚合根 TextImportService:把批次匯入書稿的存儲過程(c_textid
  配號、書名正規化與字形標準化、拼音派生、稽核蓋章)抽成單一真源,
  AdminBatchLoadBookTitlesController 改為委派、只留批次語義
- 文獻的兩重層級分開處理:TEXT_INSTANCE_DATA 版本列屬聚合內部,
  update 以 (edition_id, instance_id) 為列鍵做集合對賬、delete 先子後父;
  c_source 自引用(著錄來源樹)為跨實體引用,update 有成環護欄
  (422 source_cycle)、delete 引用計數含子文獻與其他文獻版本列
- 實體級 mutation API:resource=text-entity(別名 book/books;不用
  text——那是人物著述子資源 BIOG_TEXT_DATA 的既有別名),經通用
  EntityAggregate*Handler 分派,direct 與 proposal 皆可用
- 專屬前端頁 /app/text(Index/Create/Edit+版本列編輯器),列表走
  EntityTableBrowser、計算欄 instance_count/child_count;側欄
  「文獻代碼表」改指實體頁;新增 text_entity i18n(zh-TW/en)
- 異體字落地替換(AGENTS.md §1.3)掛在聚合根三個落點:書名(早於拼音
  派生)、主列其餘文本欄、TEXT_INSTANCE_DATA 版本列整列(c_publisher
  是不帶 _chn 的中文欄);模式與範圍一律由 VariantReplaceScope 決定,
  結果經 __variant_replaced 帶進回應 notices,累積器每次寫入前重置。
  VariantReplaceHookCoverageTest 清冊同步(掛鉤由批次控制器移入聚合根)
- step 4(封閉下層直寫)整體暫緩:codes UI 待 parity(作者列表面板、
  instance 專屬欄位);text-codes 裸表 create 因異體字 S5 剛接上落地
  替換而維持在役,與聚合並存(比照 OFFICE_CODES 拼音欄與 office 聚合)
- 版本鍵 0 值:ctype_digit() 會放行 "0",但生產庫有一列歷史資料
  (40354, 0, 0),無條件拒絕會讓該文獻無法編輯 ⇒ create 一律擋、
  update 只擋資料庫裡不存在的 0 值鍵(codex review)
- TextForm 版本列改用前端穩定 uid 當 key(AGENTS §4;不用複合鍵本身,
  那兩欄使用者可編輯,改動會整列重新掛載而丟焦點)(codex review)
- 同步 API.md §4/§13、ENTITY_AGGREGATE_ARCHITECTURE.md、CHANGELOG
- 已執行 ./vendor/bin/phpunit 全套(2977 tests,既有 SecurityAuditLogTest
  環境性失敗除外全綠)、npm run build、npm test、php-cs-fixer
@frankslin

Copy link
Copy Markdown
Contributor Author

衝突已解,已 rebase 到 d25a0d49(異體字 S9)之上並 push,現在是 MERGEABLE

因為這期間上游把異體字計畫從 S0 推到 S9,衝突不只是文字層面,有兩處語義相撞,其中一個我改了原本的做法,請 review 時特別看一下:

1. text-codes 裸表 create 保留在役(原本我下架了)

我這個 PR 原先把 TEXT_CODESconfig/code_table_writes.php 移除、讓裸表 create/delete 回 501,理由是聚合資源已完整取代。但 S5(fb9b76db)正好把落地替換接進了這條路徑,修掉 G4 不一致、讓「淸嘉錄」走 Codes UI/批次匯入/token API 三條路徑同輸入同結果,並附了測試。

解衝突時我回退了自己的下架,取上游版本,理由:

  • 關掉它本來就不在這個 PR 的目標範圍內(本 PR 是 step 3:聚合根+實體級 API+專屬頁;封閉下層直寫是 step 4);
  • 我為了 parity 已經把 codes UI 的封寫延後了,API 側一併延後前後才一致;
  • 兩條路徑並存有先例——OFFICE_CODES.c_office_pinyin 的 update 就與 office 聚合並存;
  • 否則就得刪掉剛落地的 S5 功能與其測試。

連帶把 API.md(6 處)、ApiV2MutateCodeTableTextCodesTest、架構文檔與 config 註解全部改回一致。closed_code_tables 維持空陣列,兩條裸表路徑何時封閉留作獨立決策。

2. 聚合根補上異體字落地替換掛鉤(S8 的機械化把關)

S8 把「新增文本寫入路徑必須掛替換」訂成常規並加了 VariantReplaceHookCoverageTest。新的 TextImportService 正好是這類路徑,而且我把批次控制器原本唯一的掛鉤搬進了它——清冊會直接變紅。依 S4 在官職聚合的先例掛在三個落點:

  • 書名(replaceFor早於 buildTitlePinyin()——pinyin.c_chn 在排除範圍內、異體字保有自己讀音,先替換才拿得到參考字的讀音);
  • 主列其餘文本欄(c_title_transc_title_alt_chnc_pagesc_notes);
  • TEXT_INSTANCE_DATA 版本列整列replaceRow,鍵就是欄位名)——c_publisher 正是 D3 那批不帶 _chn 後綴的中文欄。

模式與範圍一律交給 VariantReplaceScope,呼叫端不自選模式。實測 TEXT_CODES.c_title_chn 仍是 lenient(與原本硬寫的 replaceLenient 行為一致,批次匯入無行為變化),c_text_type_id(代碼鍵)與 c_url_api 則落在排除清單、傳進去是 no-op。替換結果經 __variant_replaced 帶進回應 notices;累積器在每次 create()update() 進入時重置(批次匯入用同一個 service 實例逐列呼叫)。

順帶修掉 codex 的兩則 P2(詳見各自的回覆串)。

驗證:全套 phpunit 2977 tests / 15734 assertions,除 4 個 SecurityAuditLogTest 既有失敗外全綠——那 4 個我在不含本 PR 的純上游 a821dea0 上重跑過,同樣失敗,是本機 Docker 取不到 request IP 的環境問題、與本 PR 無關。另跑 npm run buildnpm test(90)、php-cs-fixer(0 fixes)。

@sudoghut
sudoghut merged commit a83b9e5 into cbdb-project:develop Aug 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants