Skip to content

fix(rich): phase-D1 canonical closure + preflight legal-set alignment (#669) - #687

Merged
jnhu76 merged 4 commits into
masterfrom
fix/669-rich-phase-d1-canonical-closure
Oct 2, 2026
Merged

jnhu76 merged 4 commits into
masterfrom
fix/669-rich-phase-d1-canonical-closure

Conversation

@jnhu76

@jnhu76 jnhu76 commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Phase D1 of #669 — PC-F01 (RC-03) + PC-F02 (RC-04) only

D1-A restores canonical closure at the durable Rich acceptance boundary; D1-B removes the preflight's hidden stricter legal set. Evidence provenance: #686 is the Phase-C research evidence PR (closed without merge, branch preserved) — it is not a parent of this branch; D1 branches from live master 6c50ebfe (#685). Ledger context: #673. Do not merge before final review; no authority document is changed.

Root causes (as-built, confirmed against Phase-C evidence)

  • PC-F01 / RC-03: normalization merges adjacent same-mark text runs, so a schema-valid, limits-legal input (two unmarked runs, 20000 + 1 chars) canonicalizes to a 20001-char run that ContentDocumentV1Schema rejects — yet the canonicalizer returned it for persistence. Both durable writers shared the hole: the answer canonicalizer (validateAnswerForQuestion) and the question/option content seam (resolveQuestionContentWrite).
  • PC-F02 / RC-04: preflightContentDocumentStructure counted every raw JSON value (objects, arrays, scalar strings) against totalNodes * 2 + 64 = 4064, while CONTENT_LIMITS.totalNodes counts grammar nodes (a marked text run costs ~6 raw units). All 7 Phase-C shape families were rejected at ~30% of the serialized budget and well under 2000 grammar nodes.

Repair (production: 4 files)

  • D1-A — new canonicalizeContentDocument in @exam/contracts: normalize + re-validate the canonical output through the one Rich authority (ContentDocumentV1Schema = grammar + CONTENT_LIMITS). Both durable writers now persist only its accepted value; legality is decided on the representation actually stored. SaveAnswer keeps its existing INVALID_ANSWER category; authoring keeps ValidationError → 400. No second limit table, schema, normalizer, or acceptance predicate.
  • D1-B — preflight now owns exactly the recursive-parser-safety budgets (RC-04 allow-list): non-JSON/cycle rejection, serialized-size (which also bounds the walk: every raw JSON value serializes to ≥1 char), and raw nesting depth. The raw-node and fan-out budgets are deleted; grammar-node budgets remain the post-parse CONTENT_LIMITS authority. Deepest-legal nesting still passes (unchanged depth budget); hostile depth/size still rejected pre-parse.

Regression coverage (R1–R5 + property)

  • R1 PC-F01 seed rejected at the canonicalizer, authoring seam (400), and save-answer route (INVALID_ANSWER) — closes the Phase-C "L3 inferred, not executed" gap by driving the real route.
  • R2 canonical textRun boundary closed from both sides, derived from CONTENT_LIMITS (T−1+1 accepted and schema-legal; T−1+2 rejected).
  • R3 legality decided on the canonical form: duplicate-mark dedup + merge, hardBreak as a non-merge barrier, RC-03 fixed point on accepted values.
  • R4 Phase-C shrink families (plain runs, marked runs, duplicate marks, empty paragraphs, marked tables) pass preflight and the public parse entry at their measured failure scales.
  • R5 hostile nesting, oversized serialization, and node-count overflow still rejected — now named by the CONTENT_LIMITS walker instead of a preflight budget.
  • Seeded bounded property (300 cases, seed 0x66900001): every accepted answer stays schema-legal after canonicalization. Production functions only.

Historical compatibility

New durable acceptance only; no read path, classification, or persisted value is touched. Values persisted pre-fix by the PC-F01 chain were already rejected by every re-validation read path (Phase-C downstream chain); D1 does not change their read behavior. Preflight loosening is prospective: the save/authoring seams rejected over-budget documents before persisting, so no persisted value changes classification (if any pre-preflight-era value exists, the loosening can only make it readable, never unreadable). In-flight replay of an over-limit payload now hits Rich canonicalization before the replay check, per contract §13 precedence — contract-conformant, no migration.

Verification

  • Targeted: domain 23/23, contracts 20/20, API rich (lib + routes) 30/30 — red before, green after (TDD).
  • pnpm verify:static green (format, code-quality, arch, db-config, db-journal, env-contract, eslint, typecheck, openapi).
  • Full coverage: all packages green; API suite 2782 passed, 1 environmental failure — server.shutdown.test.ts EADDRINUSE :3000, because the spawned dev-mode child binds DEV_API_PORT regardless of its APP_PORT (runtimeConfig.ts port-ownership switch) and a live dev server on this host holds :3000. Unrelated to this branch; classified with evidence.
  • pnpm build green; bash scripts/e2e/run.sh rich-content 2/2 shards green.

Follow-up candidates (not bundled)

jnhu76 added 3 commits October 3, 2026 00:01
preflightContentDocumentStructure counted every raw JSON value (objects,
arrays, scalar strings) against PREFLIGHT_NODE_BUDGET = totalNodes * 2 + 64
(4064), while CONTENT_LIMITS.totalNodes counts grammar nodes: a marked text
run costs ~6 raw units, so legal documents at ~30% of the serialized budget
and well under 2000 grammar nodes were rejected before the schema parse
(#673 C1 / PC-F02, evidence #686 — 7/7 shape families HIDDEN_STRICTER_SET).

Remove the raw-node and fan-out budgets: the serialized-size gate already
bounds the walk (every raw JSON value serializes to >= 1 char), and raw
nesting depth stays bounded by PREFLIGHT_RAW_DEPTH_BUDGET, which is what
actually protects the recursive parser. Grammar-node budgets remain the
post-parse CONTENT_LIMITS authority, so preflight no longer defines a
hidden stricter legal set (RC-04).
Normalization can change the measured shape (adjacent same-mark text runs
merge, duplicate marks dedup), so a schema-valid input could normalize to a
canonical value outside CONTENT_LIMITS — accepted on write, rejected by the
same authority on every later re-validation (B-F01 / PC-F01, RC-03; the
20001-char merge class, evidence #686). Both durable Rich writers shared
the hole: the answer canonicalizer and the question/option content seam.

Add canonicalizeContentDocument to @exam/contracts — normalize + re-validate
the canonical output through the one Rich authority (ContentDocumentV1Schema
= grammar + CONTENT_LIMITS) — and route both writers through it. Legality is
now decided on the representation that is actually persisted. SaveAnswer
maps the rejection to its existing INVALID_ANSWER category; authoring maps
it to ValidationError (400). No second limit table, schema, or normalizer.
R1  PC-F01 merge-class seed (20000+1 unmarked runs) rejected at the answer
    canonicalizer, the authoring seam (400), and the save-answer route
    (INVALID_ANSWER) — the Phase-C L3 gap driven end to end.
R2  Canonical textRun boundary closed from both sides (T-1+1 accepted and
    schema-legal; T-1+2 rejected), derived from CONTENT_LIMITS.
R3  Legality decided on the canonical form: duplicate-mark dedup + merge,
    hardBreak as a non-merge barrier, RC-03 fixed point on accepted values.
R4  The seven Phase-C preflight-shrink families (plain runs, marked runs,
    duplicate marks, empty paragraphs, marked tables) pass preflight and
    the public parse entry at their measured failure scales.
R5  Genuine violations still rejected: hostile nesting, oversized
    serialization, and node-count overflow now named by the CONTENT_LIMITS
    walker instead of a preflight budget.
Plus a bounded seeded property (300 cases, seed 0x66900001): every accepted
answer stays schema-legal after canonicalization. Production functions
only — no duplicated oracle. Evidence provenance: #686 (research branch,
not a parent of this branch). #669 #673
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a60d3879-1d8a-4a36-a452-7ae8776c4b32

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jnhu76 jnhu76 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

D1 focused review: CHANGE_REQUIRED (one narrow safety blocker).

The RC-03 canonical-closure repair itself looks correct: both durable Rich writer seams now share canonicalizeContentDocument, which delegates to the existing normalizer and then re-validates the actual canonical output through ContentDocumentV1Schema; the permanent regressions cover PC-F01 at helper/authoring/real SaveAnswer-route boundaries and keep the existing error categories.

The RC-04 authority correction is also directionally correct: removing the raw-node/fan-out budgets eliminates the hidden stricter language and leaves grammar-node limits to CONTENT_LIMITS.

F1 — serialized-size gate does not actually bound/short-circuit the raw walk. In preflightContentDocumentStructure, when JSON.stringify(value).length > CONTENT_LIMITS.serializedChars, the code pushes a violation but then continues into the iterative DFS. That contradicts the new comments/tests saying the oversized value is rejected "before the raw walk runs" and weakens the preflight safety story after removing the old node/fan-out early exits. For an already-known oversized hostile value, there is no need to traverse the whole structure at all.

Required correction: once serialization succeeds and exceeds the authoritative serialized-size limit, return the violation immediately (after any envelope checks only if there is a specific contract reason to retain them; preferably keep this as the cheap structural gate). Add a regression that proves an oversized fan-out does not enter/traverse the raw DFS—not merely that a violation is eventually returned. Keep the legal-set behavior unchanged.

No broad redesign requested. After this focused fix I only need to re-review this preflight path; RC-03 does not need reopening.

F1 review finding (#687): an oversized payload had its verdict recorded,
but the raw DFS still traversed the hostile structure — the serialized
budget was not actually the traversal bound the section comment claimed.

An oversized payload is now rejected before the walk; envelope
diagnostics (O(1) property reads, existing error contract) are kept.
The regression test pairs oversize with beyond-raw-depth nesting and
asserts the size violation is the ONLY output, proving the walk never
ran.
@jnhu76

jnhu76 commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

F1 修复:preflight 序列化门禁短路(commit 38b6b735)

修复内容(仅此一处 + 对应 regression,未触碰 canonicalization):

preflightContentDocumentStructure 现在在 serialized-size gate 命中后、进入 raw DFS 前短路返回。envelope 诊断(3 个 O(1) 属性读取)按现有 error contract 保留;被移除的只有「对已判非法的超限 hostile 结构继续遍历」这一行为。section 注释声称的 traversal bound 现在由实现真实成立。

Regression test:stops at the serialization gate — an oversized payload never enters the raw walk

  • fixture:合法 envelope + 50 层嵌套数组(raw depth 52 > 39 预算)+ 底部 200k 字符(serialized ≈ 200k > 131072)——超限与超深双违例;
  • 断言 toEqual 精确等于 [serialized violation]:若 walk 运行过会追加 nesting exceeds violation;
  • 修复前红灯实证:["serialized document exceeds 131072 chars", "document nesting exceeds 39 levels"](walk 确实跑了)——修复后仅返回 size violation。

验证(focused,按 BROAD_RERUN_REQUIRED = NO):

  • domain contentDocument.test.ts 24/24;contracts(重建 dist 后)20/20;API rich 相关(validateAnswerForQuestion / questionRichContent / answerRichClosure)30/30;
  • pnpm verify:static exit=0(format / eslint / arch / typecheck / openapi up-to-date)。

未重跑 coverage/E2E:改动限于 domain 单函数的拒绝路径诊断集缩减,所有消费方(contracts refinement、web 预检、API writer)只读 violation 列表,无对「超限文档附带 depth violation」的依赖。

@jnhu76 jnhu76 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Focused re-review of F1 follow-up (38b6b735) only.

Verdict: F1 CLOSED.

The implementation now short-circuits after the serialized-size verdict and before the raw DFS. Envelope checks remain O(1), while oversized hostile structure is no longer traversed. The new dual-violation regression (oversize + raw depth > budget) is a good behavioral proof: it requires the result to contain only the serialized-size violation, so any accidental DFS would reintroduce the depth violation and fail the test.

No canonicalization code was touched; the previously accepted RC-03/RC-04 repair remains unchanged.

Focused verdict:

F1_OVERSIZED_PREFLIGHT_WALK = CLOSED
RC-03_CANONICAL_CLOSURE     = PASS
RC-04_LEGAL_SET_ALIGNMENT   = PASS
PRELIGHT_SAFETY             = PASS
PHASE_D1_CONTENT            = PASS

Exact-head CI for 38b6b735 is still in progress at review time, so merge should wait for that run to finish green. No broad rerun or further D1 code change is requested.

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.

1 participant