fix(export): #669 Phase D4 — Rich export trust boundary (F-05/F-08) - #692
Merged
Merged
Conversation
Move classifyPersistedRichAnswer / RichAnswerReadState / resolveRichAnswerDocument from apps/web into @exam/contracts so the web read paths and the API export boundary consume ONE semantic implementation (#669 Phase D4, F-08). The classifier bodies are byte-identical to the D3 web implementation; only the module location changed. contentDocumentsEqual moves to @exam/domain (the §18 equivalence authority) because the shared classifier's canonical-trust comparison needs it and a second implementation is forbidden; the editor keeps importing the same function from its new home, and its tests moved with it. The web richAnswer module is deleted; consumers import from @exam/contracts. No behavior change intended: the moved classifier suite and the component-level fail-closed tests pass unchanged (readTrust, editor ownership, re-pointed pages).
F-05 is a frozen-contract violation, not a new export feature: CSV export decided Rich validity from the runtime shape (typeof value === "string"), so a Rich slot holding a corrupt/unprovenanced string was silently exported as normal Plain answer text (rich-content-semantic-contract §14). The export boundary now classifies every candidate answer through the one shared persisted-answer classifier against the FROZEN snapshot answerMode (never the live question bank): - text_response slots classify into the seven §7 read states; unsupported / corrupt values get NO semantic projection (CSV marker, never stored text); rich_valid / rich_noncanonical expose the same plain projection the read paths display; legacy_plain stays provenance-gated (unreachable until a proven producer exists); - typed objective slots keep their existing display rule (not Rich slots, so the classifier cannot mislabel arrays/booleans as corrupt). JSON keeps candidateAnswer as the untouched raw evidence and adds candidateAnswerMode / candidateAnswerIntegrity / candidateAnswerProjection (OpenAPI regenerated). CSV appends 考生答案模式 / 考生答案状态 after its frozen columns so positional consumers keep their offsets; the answer cell is the classified projection only. Export stays read-only: no normalization, repair, or write-back.
Exercise the real export seams (both routes through the frozen attempt snapshot), not only the classifier: - R1/F-05 counterexample: a string in a frozen rich slot stays raw evidence in JSON, classifies corrupt, and never reaches the CSV as answer text; - R2: plain and typed objective answers still export normally; - R4: canonical Rich preserves raw evidence and labels its derived projection rich_valid; - R5: noncanonical Rich is projected but never relabeled or repair-written; - R6/R7: unsupported docVersion and malformed envelopes keep raw evidence with no fabricated Plain projection; - D4-G: exporting after mutating the LIVE question row proves the frozen snapshot answerMode is the classification source; - empty: an absent answer exports as empty with no projection. The structural ownership guard pins D4-A/D4-K: classifyPersistedRichAnswer is defined exactly once (in @exam/contracts), and the export seam does not parse documents itself (no shape oracle).
Record the as-built export shape in §14 (JSON raw + integrity/projection companions; CSV appended mode/state columns with the classified answer cell) and point §18 at the single shared classifier in packages/contracts/src/persistedRichAnswer.ts. The frozen §7/§14 semantics are unchanged: corrupt Rich must never be exported as a valid Plain projection, and this change repairs the implementation to match.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
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. Comment |
Owner
Author
|
Focused independent D4 review: PASS. Verified at exact head
Verdict: No D5/F-06 work should be bundled into this PR. Note: GitHub does not allow the authenticated PR author to approve their own PR, so this is recorded as the independent review disposition rather than an APPROVE review event. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase D4 — Rich export trust boundary (#669)
Implements Phase D4 only. D4 is driven by the post-D2 Authority ↔ Implementation adversarial audit.
F-05 is a frozen-contract violation, not a new export feature request. CSV export decided Rich validity from the runtime shape (
typeof value === "string"), so a Rich slot holding a corrupt/unprovenanced string was silently exported as normal human-readable Plain answer text — violating the frozen §7/§14 rule that invalid/corrupt Rich must never be silently exported as a valid Plain projection. D4 also resolves the adjacent traceability gap F-08 (the seven-state classifier lived only inapps/web).What changed
One shared classifier (D4-A / D4-K, F-08)
classifyPersistedRichAnswer/resolveRichAnswerDocumentmoved to@exam/contracts(packages/contracts/src/persistedRichAnswer.ts); the web module is deleted andRichTextAnswerInput+AttemptDetailPage/GradingDetailPage/ResultPageimport it. Classifier bodies are byte-identical to the D3 implementation (verified by diff); the moved D3 suite passes unchanged.contentDocumentsEqualmoved to@exam/domain(the §18 equivalence authority) so the classifier and the editor share one comparator; its tests moved with it.persistedRichAnswerOwnership.structural.test.ts): exactly one definition repo-wide; the export seam must not parse documents itself.Trust-aware export (D4-B…D4-I)
apps/api/src/lib/attemptExportAnswer.ts: each candidate answer classifies through the shared classifier against the frozen snapshotanswerMode(never the live question bank).text_responseslots classify into the seven §7 states; typed objective slots keep their existing display rule (not Rich slots — the classifier cannot mislabel arrays/booleans as corrupt).candidateAnswerstays the untouched raw evidence; additivecandidateAnswerMode/candidateAnswerIntegrity/candidateAnswerProjection. OpenAPI regenerated.考生答案模式/考生答案状态after the frozen columns (existing positions unchanged → positional consumers preserved). The考生答案cell holds the classified projection only —unsupported_version/corruptrender the not-applicable marker, never stored text; raw evidence stays in the JSON export route.Regressions (real seam, not only the classifier)
R1/F-05 counterexample (string in frozen rich slot → corrupt, never CSV text) · R2 plain + typed objective controls · R4 rich_valid raw+labeled projection · R5 noncanonical projected & unrepaired (DB re-read) · R6 unsupported_version no fake Plain · R7 malformed envelope corrupt · D4-G live-question mutation proves frozen-mode authority · empty state · extraction-equivalence (moved D3 corpus + component fail-closed tests).
Non-goals
No D5 prompt renderer / KaTeX work; F-06 stays → D5-A. No new Rich grammar or ContentDocument version; no SaveAnswer replay / receipt / lifecycle changes; no candidate UX redesign (import paths only); no capability composition work.
Verification
pnpm test(16/16 tasks, API 2801 passed) ·pnpm verify:static·pnpm verify(coverage + build) · exact-head CI pending on this head.Refs #669 #673 · Phase D2 (#691) · Phase D3 (#690) · F-05 · F-08