实现: 装配正文五章真实冻结评测
将卡稳定选择器、冻结历史正文和目标 scaffold 绑定到同一只读快照;固定上下文预算并净化诊断索引,收紧盲评输入边界,防止目标事实和选卡信息泄漏。
This commit is contained in:
parent
b9ff4d0b40
commit
fa922f8cc5
@ -392,9 +392,22 @@ def _index_hint(card: Mapping[str, Any], *, as_of: int) -> dict[str, Any]:
|
|||||||
latest = state_as_of[-1]
|
latest = state_as_of[-1]
|
||||||
if not isinstance(latest, Mapping):
|
if not isinstance(latest, Mapping):
|
||||||
raise RetrievalError(f"卡 {card.get('cardId')} 最新冻结状态非法")
|
raise RetrievalError(f"卡 {card.get('cardId')} 最新冻结状态非法")
|
||||||
content = normalize_text(
|
if card.get("sourceKind") == "eval_draft":
|
||||||
str(latest.get("fact") or latest.get("台阶") or canonical_json(latest))
|
# 诊断卡只能公开索引元数据;里程碑台阶属于被测信息,不能再透传给 B/C 臂。
|
||||||
)
|
content = canonical_json(
|
||||||
|
{
|
||||||
|
"name": str(card.get("name") or ""),
|
||||||
|
"type": str(card.get("type") or ""),
|
||||||
|
"sourceChapters": sorted(
|
||||||
|
{_chapter(ref.get("chapter"), "card.sourceRefs.chapter") for ref in card.get("sourceRefs", [])}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 生产 Canonical 卡保持原有冻结状态提示行为。
|
||||||
|
content = normalize_text(
|
||||||
|
str(latest.get("fact") or latest.get("台阶") or canonical_json(latest))
|
||||||
|
)
|
||||||
hint = {
|
hint = {
|
||||||
"cardId": str(card.get("cardId") or ""),
|
"cardId": str(card.get("cardId") or ""),
|
||||||
"name": str(card.get("name") or ""),
|
"name": str(card.get("name") or ""),
|
||||||
@ -491,7 +504,11 @@ def retrieve_writer_sources(
|
|||||||
"chapter": _chapter(row.get("chapter"), "prose.chapter"),
|
"chapter": _chapter(row.get("chapter"), "prose.chapter"),
|
||||||
"sourceRef": copy.deepcopy(ref),
|
"sourceRef": copy.deepcopy(ref),
|
||||||
"contentSha256": _content_hash(text),
|
"contentSha256": _content_hash(text),
|
||||||
"purpose": str(row.get("purpose") or "card_source"),
|
"purpose": (
|
||||||
|
"card_chapter_proxy"
|
||||||
|
if ref.get("sourceType") == "card_chapter_proxy"
|
||||||
|
else str(row.get("purpose") or "card_source")
|
||||||
|
),
|
||||||
"text": text,
|
"text": text,
|
||||||
"isRecentBaseline": False,
|
"isRecentBaseline": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -292,6 +292,29 @@ class RetrieveWriterSourcesTest(unittest.TestCase):
|
|||||||
[{"sourceId": "canonical-entity:2", "reason": "missing_source_refs"}],
|
[{"sourceId": "canonical-entity:2", "reason": "missing_source_refs"}],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_eval_draft_hint_only_contains_index_metadata_and_proxy_is_explicit(self):
|
||||||
|
"""评测卡不透传里程碑台阶,整章代理必须保留降级来源和用途。"""
|
||||||
|
|
||||||
|
replay_card = card("1", 0.9)
|
||||||
|
replay_card["sourceKind"] = "eval_draft"
|
||||||
|
replay_card["sourceRefs"][0]["sourceType"] = "card_chapter_proxy"
|
||||||
|
result = retrieve_writer_sources(
|
||||||
|
plan=self.plan,
|
||||||
|
card_repository=FakeCardRepository([replay_card]),
|
||||||
|
prose_repository=FakeProseRepository(),
|
||||||
|
)
|
||||||
|
hint_content = result["indexHints"][0]["content"]
|
||||||
|
self.assertEqual(
|
||||||
|
hint_content,
|
||||||
|
'{"name":"角色1","sourceChapters":[3],"type":"character"}',
|
||||||
|
)
|
||||||
|
self.assertNotIn("冻结线内状态", hint_content)
|
||||||
|
self.assertEqual(
|
||||||
|
result["proseEvidence"][0]["sourceRef"]["sourceType"],
|
||||||
|
"card_chapter_proxy",
|
||||||
|
)
|
||||||
|
self.assertEqual(result["proseEvidence"][0]["purpose"], "card_chapter_proxy")
|
||||||
|
|
||||||
def test_snapshot_transaction_is_repeatable_read_and_read_only(self):
|
def test_snapshot_transaction_is_repeatable_read_and_read_only(self):
|
||||||
connection = FakeConnection()
|
connection = FakeConnection()
|
||||||
begin_read_snapshot(connection)
|
begin_read_snapshot(connection)
|
||||||
|
|||||||
@ -61,6 +61,8 @@ disable-model-invocation: true
|
|||||||
|
|
||||||
正文 Gate A 固定配置为 `configs/writer-gate-a-deep-space-v1.json`,预注册深空之影五章:489 战斗、321 人物对话、544 转折、199 信息揭示、523 老角色回归。不得根据候选结果换章、换场景分类或修改冻结点;每个冻结点必须等于 `targetChapter-1`。
|
正文 Gate A 固定配置为 `configs/writer-gate-a-deep-space-v1.json`,预注册深空之影五章:489 战斗、321 人物对话、544 转折、199 信息揭示、523 老角色回归。不得根据候选结果换章、换场景分类或修改冻结点;每个冻结点必须等于 `targetChapter-1`。
|
||||||
|
|
||||||
|
真实五章装配还必须在 `commonControls` 预注册选择器版本、选择器规范 JSON 的原始字节 SHA-256、`inputProvenance=oracle_reference_scaffold` 和统一 `maxContextChars=140000`。loader 在数据库读取前机械核对这些值:选择器篡改、来源标记变化或样本预算与公共控制不一致都失败关闭;loader 不得根据实际文本抬高预算。连续四章基线装不下时由 assembler 阻断,弱补充原文由 assembler 按预算裁剪。
|
||||||
|
|
||||||
每个样本必须提供 `writerContextInput`。仓内配置只允许 `contentMode=sanitized_contract_fixture`,`recentChapters` 只能放连续四章的脱敏合成短文本,用于证明 WriterContext 合同、章号冻结和 A/B/C 差异策略;它不是历史原文,不证明真实正文完整性或文学质量。配置可以记录细纲硬约束、主要实体、预期篇幅、新角色比例和泄漏哨兵,但不得包含原书全文、完整目标细纲或标准答案。
|
每个样本必须提供 `writerContextInput`。仓内配置只允许 `contentMode=sanitized_contract_fixture`,`recentChapters` 只能放连续四章的脱敏合成短文本,用于证明 WriterContext 合同、章号冻结和 A/B/C 差异策略;它不是历史原文,不证明真实正文完整性或文学质量。配置可以记录细纲硬约束、主要实体、预期篇幅、新角色比例和泄漏哨兵,但不得包含原书全文、完整目标细纲或标准答案。
|
||||||
|
|
||||||
三臂都必须构造并校验 `WriterContext v1`,且固定 `mode=diagnostic_only`、`purpose=evaluation`、`acceptanceEligible=false`:
|
三臂都必须构造并校验 `WriterContext v1`,且固定 `mode=diagnostic_only`、`purpose=evaluation`、`acceptanceEligible=false`:
|
||||||
@ -69,13 +71,15 @@ disable-model-invocation: true
|
|||||||
- B:`evidenceStrategy=card_index_only`,只放 `retrievalResult.indexHints`,`proseEvidence` 为空;这是合同内可审计的诊断基线例外,不是生产旁路。
|
- B:`evidenceStrategy=card_index_only`,只放 `retrievalResult.indexHints`,`proseEvidence` 为空;这是合同内可审计的诊断基线例外,不是生产旁路。
|
||||||
- C:`evidenceStrategy=card_index_plus_prose`,保留连续四章脱敏基线,并放冻结 `indexHints` 与配置中的补充原文证据。
|
- C:`evidenceStrategy=card_index_plus_prose`,保留连续四章脱敏基线,并放冻结 `indexHints` 与配置中的补充原文证据。
|
||||||
|
|
||||||
`indexHints` 每项只能包含 `cardId/name/type/content/sourceId/sourceVersion/asOf`,只能用于 `diagnostic_only` 的 `evaluation/diagnostic`,不得进入生产上下文;`claimLedger` 不得引用它。所有 `asOf` 和来源章号不得超过样本冻结点。
|
`indexHints` 每项只能包含 `cardId/name/type/content/sourceId/sourceVersion/asOf`,只能用于 `diagnostic_only` 的 `evaluation/diagnostic`,不得进入生产上下文;`claimLedger` 不得引用它。eval_draft 的 `content` 只能编码卡名、类型和实际回读章号,不得透传里程碑台阶;生产 Canonical 卡行为不变。所有 `asOf` 和来源章号不得超过样本冻结点。
|
||||||
|
|
||||||
|
卡没有持久化 `sourceRefs` 时,允许把冻结线内最近三个里程碑章的整章 Canonical block 作为降级回读,但每个引用必须标记 `sourceType=card_chapter_proxy`,对应 `proseEvidence.purpose=card_chapter_proxy`,不得冒充精确片段。每个代理章必须实际出现卡规范名或选择器预注册的合法别名,否则失败关闭。
|
||||||
|
|
||||||
每个样本必须记录 `frozenRecentHanCounts` 和 `targetLengthBasis`。篇幅只能使用冻结点前连续四章汉字数,经 `calculate_target_chars` 复算;当前 Gate A 固定 `hardEventCount=1`、`foreshadowingActionCount=0`、`requiredSceneCount=0`,禁止读取目标章实际长度。`expectedLength` 与 WriterContext 的输出合同必须等于机械目标的正负 10%,再受 2000-10000 边界限制。
|
每个样本必须记录 `frozenRecentHanCounts` 和 `targetLengthBasis`。篇幅只能使用冻结点前连续四章汉字数,经 `calculate_target_chars` 复算;当前 Gate A 固定 `hardEventCount=1`、`foreshadowingActionCount=0`、`requiredSceneCount=0`,禁止读取目标章实际长度。`expectedLength` 与 WriterContext 的输出合同必须等于机械目标的正负 10%,再受 2000-10000 边界限制。
|
||||||
|
|
||||||
`newCharacterRatio` 定义为:具名 `requiredCharacters` 中,在 `asOfChapter` 前无记录的角色比例。泛称角色不参与猜测;例如第 544 章的“内应”无法确定具体身份,必须记录 `newCharacterRatio=null` 和 `newCharacterRatioStatus=unresolved_generic_role`,不得默认成 0。
|
`newCharacterRatio` 定义为:具名 `requiredCharacters` 中,在 `asOfChapter` 前无记录的角色比例。真实 loader 必须在同一 `REPEATABLE READ READ ONLY` 事务内直接扫描冻结历史 Canonical 正文,按名字返回首次命中章和命中章集合,再重算 `knownBeforeAsOf/absentBeforeAsOf/newCharacterRatio`;卡内容不得参与这个判定。泛称角色不参与猜测;例如第 544 章的“内应”无法确定具体身份,必须记录 `newCharacterRatio=null` 和 `newCharacterRatioStatus=unresolved_generic_role`,不得默认成 0。
|
||||||
|
|
||||||
盲评使用独立的 `writer-blind-input-v1` 内容边界。judge 只能看到 `blind-1/2/3` 候选正文/哈希,以及所有臂完全相同的 `sharedEvaluationReference`:目标章细纲硬约束、实体、必需角色、伏笔、章末钩子和冻结点前连续四章历史原文基准。共同参考可以在真实运行时临时传给 judge,但不能进入安全摘要。它不得包含 `indexHints`、各臂补充原文、卡 manifest、`evidenceStrategy`、真实 A/B/C 映射、各臂 WriterContext、`raw` 目录或任何包含 `candidate-A/B/C` 的路径;卡的正确性只能由共同历史原文基准验证,不能把被测卡本身当裁判。映射只保留在编排器内存中,评分完成后才去盲。
|
盲评使用独立的 `writer-blind-input-v1` 内容边界。judge 只能看到 `blind-1/2/3` 候选正文/哈希,以及所有臂完全相同的 `sharedEvaluationReference`:细纲严格四字段 `sourceRef/hardConstraints/adjustableBeats/declaredNewFacts`、必需角色、伏笔、章末钩子和冻结点前连续四章历史原文基准;不得增加 `entities`。共同参考可以在真实运行时临时传给 judge,但不能进入安全摘要。它不得包含 `indexHints`、各臂补充原文、卡 manifest、`evidenceStrategy`、真实 A/B/C 映射、各臂 WriterContext、`raw` 目录或任何包含 `candidate-A/B/C` 的路径;卡的正确性只能由共同历史原文基准验证,不能把被测卡本身当裁判。映射只保留在编排器内存中,评分完成后才去盲。
|
||||||
|
|
||||||
正文 dry-run 命令:
|
正文 dry-run 命令:
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"selectorVersion": "writer-gate-a-deep-space-card-selectors-v1",
|
||||||
|
"evaluationSetVersion": "writer-gate-a-deep-space-v1",
|
||||||
|
"workId": 8,
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"sampleId": "deep-space-489-battle",
|
||||||
|
"targetChapter": 489,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "location",
|
||||||
|
"name": "圣蒂曼行星"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "deep-space-321-character-dialogue",
|
||||||
|
"targetChapter": 321,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "character",
|
||||||
|
"name": "莫妮卡"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "deep-space-544-turning-point",
|
||||||
|
"targetChapter": 544,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "location",
|
||||||
|
"name": "迷途之地"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "deep-space-199-information-reveal",
|
||||||
|
"targetChapter": 199,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "character",
|
||||||
|
"name": "赛莉丝"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"name": "联赛系统被机械一族入侵",
|
||||||
|
"sourceAliases": [
|
||||||
|
"联赛系统"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sampleId": "deep-space-523-returning-character",
|
||||||
|
"targetChapter": 523,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "character",
|
||||||
|
"name": "伊蕾莉雅"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -43,6 +43,10 @@
|
|||||||
},
|
},
|
||||||
"commonControls": {
|
"commonControls": {
|
||||||
"workId": 8,
|
"workId": 8,
|
||||||
|
"selectorVersion": "writer-gate-a-deep-space-card-selectors-v1",
|
||||||
|
"selectorSha256": "sha256:9df06f7d5864696deda3dd119f8cbded01ba9a9c7e5bf5655553df938f267691",
|
||||||
|
"inputProvenance": "oracle_reference_scaffold",
|
||||||
|
"maxContextChars": 140000,
|
||||||
"modelVersion": "writer-model-unwired",
|
"modelVersion": "writer-model-unwired",
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
@ -248,7 +252,7 @@
|
|||||||
"newSettingDeclarationRequired": true
|
"newSettingDeclarationRequired": true
|
||||||
},
|
},
|
||||||
"tokenBudget": {
|
"tokenBudget": {
|
||||||
"maxContextChars": 50000
|
"maxContextChars": 140000
|
||||||
},
|
},
|
||||||
"generatedAt": "2026-07-20T00:00:00Z",
|
"generatedAt": "2026-07-20T00:00:00Z",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
@ -475,7 +479,7 @@
|
|||||||
"newSettingDeclarationRequired": true
|
"newSettingDeclarationRequired": true
|
||||||
},
|
},
|
||||||
"tokenBudget": {
|
"tokenBudget": {
|
||||||
"maxContextChars": 50000
|
"maxContextChars": 140000
|
||||||
},
|
},
|
||||||
"generatedAt": "2026-07-20T00:00:00Z",
|
"generatedAt": "2026-07-20T00:00:00Z",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
@ -709,7 +713,7 @@
|
|||||||
"newSettingDeclarationRequired": true
|
"newSettingDeclarationRequired": true
|
||||||
},
|
},
|
||||||
"tokenBudget": {
|
"tokenBudget": {
|
||||||
"maxContextChars": 50000
|
"maxContextChars": 140000
|
||||||
},
|
},
|
||||||
"generatedAt": "2026-07-20T00:00:00Z",
|
"generatedAt": "2026-07-20T00:00:00Z",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
@ -943,7 +947,7 @@
|
|||||||
"newSettingDeclarationRequired": true
|
"newSettingDeclarationRequired": true
|
||||||
},
|
},
|
||||||
"tokenBudget": {
|
"tokenBudget": {
|
||||||
"maxContextChars": 50000
|
"maxContextChars": 140000
|
||||||
},
|
},
|
||||||
"generatedAt": "2026-07-20T00:00:00Z",
|
"generatedAt": "2026-07-20T00:00:00Z",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
@ -1177,7 +1181,7 @@
|
|||||||
"newSettingDeclarationRequired": true
|
"newSettingDeclarationRequired": true
|
||||||
},
|
},
|
||||||
"tokenBudget": {
|
"tokenBudget": {
|
||||||
"maxContextChars": 50000
|
"maxContextChars": 140000
|
||||||
},
|
},
|
||||||
"generatedAt": "2026-07-20T00:00:00Z",
|
"generatedAt": "2026-07-20T00:00:00Z",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
|
|||||||
1283
.claude/skills/replay-eval/scripts/load_writer_reference_work.py
Normal file
1283
.claude/skills/replay-eval/scripts/load_writer_reference_work.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -67,6 +67,13 @@ SNAPSHOT_COMPAT_ARMS = (
|
|||||||
"outline_plus_placebo_cards",
|
"outline_plus_placebo_cards",
|
||||||
)
|
)
|
||||||
CONTENT_MODES = frozenset({"sanitized_contract_fixture", "canonical_frozen_prose"})
|
CONTENT_MODES = frozenset({"sanitized_contract_fixture", "canonical_frozen_prose"})
|
||||||
|
# Judge 只能复用写手细纲合同中的四个字段,不能直接信任原始评测配置。
|
||||||
|
FINE_OUTLINE_JUDGE_FIELDS = (
|
||||||
|
"sourceRef",
|
||||||
|
"hardConstraints",
|
||||||
|
"adjustableBeats",
|
||||||
|
"declaredNewFacts",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WriterReplayError(ValueError):
|
class WriterReplayError(ValueError):
|
||||||
@ -768,6 +775,7 @@ def _candidate_summary(candidate: Mapping[str, Any], arm: str) -> dict[str, Any]
|
|||||||
def _build_blind_input(
|
def _build_blind_input(
|
||||||
*,
|
*,
|
||||||
raw_sample: Mapping[str, Any],
|
raw_sample: Mapping[str, Any],
|
||||||
|
writer_fine_outline: Mapping[str, Any],
|
||||||
candidates: Mapping[str, Mapping[str, Any]],
|
candidates: Mapping[str, Mapping[str, Any]],
|
||||||
mapping: Mapping[str, str],
|
mapping: Mapping[str, str],
|
||||||
blind_id: str,
|
blind_id: str,
|
||||||
@ -798,10 +806,14 @@ def _build_blind_input(
|
|||||||
)
|
)
|
||||||
context_input = _mapping(raw_sample.get("writerContextInput"), "writerContextInput")
|
context_input = _mapping(raw_sample.get("writerContextInput"), "writerContextInput")
|
||||||
requirements = _mapping(context_input.get("requirements"), "writerContextInput.requirements")
|
requirements = _mapping(context_input.get("requirements"), "writerContextInput.requirements")
|
||||||
|
if set(writer_fine_outline) != set(FINE_OUTLINE_JUDGE_FIELDS):
|
||||||
|
raise WriterReplayError("写手细纲字段必须严格匹配 judge 共同参考合同")
|
||||||
shared_reference = {
|
shared_reference = {
|
||||||
"fineOutline": copy.deepcopy(
|
# 只复制写手已通过合同校验的四字段视图,禁止从原始 fineOutline 倾倒评委不可见字段。
|
||||||
_mapping(context_input.get("fineOutline"), "writerContextInput.fineOutline")
|
"fineOutline": {
|
||||||
),
|
field: copy.deepcopy(writer_fine_outline[field])
|
||||||
|
for field in FINE_OUTLINE_JUDGE_FIELDS
|
||||||
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"requiredEvents": copy.deepcopy(
|
"requiredEvents": copy.deepcopy(
|
||||||
_sequence(requirements.get("requiredEvents"), "requirements.requiredEvents")
|
_sequence(requirements.get("requiredEvents"), "requirements.requiredEvents")
|
||||||
@ -905,11 +917,24 @@ def _execute_sample_for_test(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapping, blind_order = _blind_mapping(evaluation_set_version, str(prepared["sampleId"]))
|
mapping, blind_order = _blind_mapping(evaluation_set_version, str(prepared["sampleId"]))
|
||||||
|
# 共同参考取自实际写手上下文,并在进入 judge 前确认三臂视图完全一致。
|
||||||
|
writer_fine_outline = _mapping(
|
||||||
|
prepared["_contexts"][REQUIRED_ARMS[0]].get("fineOutline"),
|
||||||
|
"writerContext.fineOutline",
|
||||||
|
)
|
||||||
|
for arm in REQUIRED_ARMS[1:]:
|
||||||
|
current_fine_outline = _mapping(
|
||||||
|
prepared["_contexts"][arm].get("fineOutline"),
|
||||||
|
f"writerContext.{arm}.fineOutline",
|
||||||
|
)
|
||||||
|
if current_fine_outline != writer_fine_outline:
|
||||||
|
raise WriterReplayError("三臂写手细纲不一致,不能构造共同评测参考")
|
||||||
reviews: dict[str, Any] = {}
|
reviews: dict[str, Any] = {}
|
||||||
first_reports: list[Mapping[str, Any]] = []
|
first_reports: list[Mapping[str, Any]] = []
|
||||||
for blind_id in blind_order:
|
for blind_id in blind_order:
|
||||||
first_input = _build_blind_input(
|
first_input = _build_blind_input(
|
||||||
raw_sample=raw_sample,
|
raw_sample=raw_sample,
|
||||||
|
writer_fine_outline=writer_fine_outline,
|
||||||
candidates=raw_candidates,
|
candidates=raw_candidates,
|
||||||
mapping=mapping,
|
mapping=mapping,
|
||||||
blind_id=blind_id,
|
blind_id=blind_id,
|
||||||
@ -919,6 +944,7 @@ def _execute_sample_for_test(
|
|||||||
first_reports.append(first)
|
first_reports.append(first)
|
||||||
second_input = _build_blind_input(
|
second_input = _build_blind_input(
|
||||||
raw_sample=raw_sample,
|
raw_sample=raw_sample,
|
||||||
|
writer_fine_outline=writer_fine_outline,
|
||||||
candidates=raw_candidates,
|
candidates=raw_candidates,
|
||||||
mapping=mapping,
|
mapping=mapping,
|
||||||
blind_id=blind_id,
|
blind_id=blind_id,
|
||||||
@ -929,6 +955,7 @@ def _execute_sample_for_test(
|
|||||||
if adjudication.get("status") == "needs_third_reviewer":
|
if adjudication.get("status") == "needs_third_reviewer":
|
||||||
third_input = _build_blind_input(
|
third_input = _build_blind_input(
|
||||||
raw_sample=raw_sample,
|
raw_sample=raw_sample,
|
||||||
|
writer_fine_outline=writer_fine_outline,
|
||||||
candidates=raw_candidates,
|
candidates=raw_candidates,
|
||||||
mapping=mapping,
|
mapping=mapping,
|
||||||
blind_id=blind_id,
|
blind_id=blind_id,
|
||||||
|
|||||||
@ -0,0 +1,558 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Writer Gate A 真实五章临时配置装配器测试。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
|
||||||
|
READ_CONTEXT_SCRIPTS = SCRIPT_DIR.parents[1] / "read-context" / "scripts"
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
sys.path.insert(0, str(READ_CONTEXT_SCRIPTS))
|
||||||
|
|
||||||
|
import load_writer_reference_work as loader # noqa: E402
|
||||||
|
from load_writer_reference_work import ( # noqa: E402
|
||||||
|
WriterReferenceWorkError,
|
||||||
|
assemble_writer_gate_config,
|
||||||
|
load_writer_reference_rows,
|
||||||
|
milestone_reference_chapters,
|
||||||
|
resolve_card_selectors,
|
||||||
|
write_temporary_config,
|
||||||
|
)
|
||||||
|
from run_writer_replay import run_writer_replay # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
BASE_CONFIG_PATH = SCRIPT_DIR.parent / "configs" / "writer-gate-a-deep-space-v1.json"
|
||||||
|
SELECTOR_PATH = (
|
||||||
|
SCRIPT_DIR.parent / "configs" / "writer-gate-a-deep-space-card-selectors-v1.json"
|
||||||
|
)
|
||||||
|
SELECTOR_DIGEST = loader.selector_sha256(SELECTOR_PATH.read_bytes())
|
||||||
|
FILE_HASH = "02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4"
|
||||||
|
SOURCE_HASH = f"sha256:{FILE_HASH}"
|
||||||
|
SOURCE_VERSION = f"raw-file-v1:{SOURCE_HASH}"
|
||||||
|
|
||||||
|
|
||||||
|
def _card_row(
|
||||||
|
card_id: int,
|
||||||
|
card_type: str,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
aliases: list[str] | None = None,
|
||||||
|
milestones: list[dict[str, object]] | None = None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""构造符合 upgrade_book 冻结投影合同的卡行。"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": card_id,
|
||||||
|
"status": "pending",
|
||||||
|
"source_type": "upgrade_book",
|
||||||
|
"source_id": 8,
|
||||||
|
"revision": 1,
|
||||||
|
"deleted": False,
|
||||||
|
"draft_payload": {
|
||||||
|
"type": card_type,
|
||||||
|
"名称": name,
|
||||||
|
"别名": aliases or [],
|
||||||
|
"出场章": [],
|
||||||
|
"字段": {
|
||||||
|
"演变历程": milestones or [{"章": 1, "台阶": f"{name}历史事实"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_cards() -> list[dict[str, object]]:
|
||||||
|
"""构造五样本固定选择所需的六张唯一卡。"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
_card_row(101, "location", "圣蒂曼行星", milestones=[{"章": 487, "台阶": "历史地点"}]),
|
||||||
|
_card_row(102, "character", "莫妮卡", milestones=[{"章": 317, "台阶": "历史人物"}]),
|
||||||
|
_card_row(103, "location", "迷途之地", milestones=[{"章": "539-542", "台阶": "历史地点"}]),
|
||||||
|
_card_row(104, "character", "璐茜", aliases=["赛莉丝"], milestones=[{"章": 190, "台阶": "历史身份"}]),
|
||||||
|
_card_row(105, "event", "联赛系统被机械一族入侵", milestones=[{"章": "188-189", "台阶": "历史事件"}]),
|
||||||
|
_card_row(106, "character", "伊蕾莉雅", milestones=[{"章": "460-462", "台阶": "历史人物"}]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _authorization() -> dict[str, object]:
|
||||||
|
"""构造通过既有授权门禁的真实原文件授权投影。"""
|
||||||
|
|
||||||
|
snapshot = {
|
||||||
|
"id": "1",
|
||||||
|
"version": "auth-work-8-test-v1",
|
||||||
|
"immutable": True,
|
||||||
|
"sourceHash": SOURCE_HASH,
|
||||||
|
"sourceVersion": SOURCE_VERSION,
|
||||||
|
"sourceStatus": "active",
|
||||||
|
"copyrightStatus": "research_only",
|
||||||
|
"allowedPurpose": ["offline_evaluation"],
|
||||||
|
"forbiddenPurpose": ["external_distribution"],
|
||||||
|
"authorizationBasis": "user_authorization",
|
||||||
|
"authorizedBy": "user:1",
|
||||||
|
"displaySummary": "仅供离线评测",
|
||||||
|
"checkedAt": "2026-07-19T00:00:00+00:00",
|
||||||
|
"expiresAt": None,
|
||||||
|
"revalidationAt": "2099-07-19T00:00:00+00:00",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"sourceStatus": "active",
|
||||||
|
"copyrightStatus": "research_only",
|
||||||
|
"sourceHash": SOURCE_HASH,
|
||||||
|
"sourceVersion": SOURCE_VERSION,
|
||||||
|
"allowedPurpose": ["offline_evaluation"],
|
||||||
|
"forbiddenPurpose": ["external_distribution"],
|
||||||
|
"authorizationSnapshot": snapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _block_row(chapter: int, text: str | None = None) -> dict[str, object]:
|
||||||
|
"""构造单章唯一 Canonical block;默认正文长度只用于小型测试。"""
|
||||||
|
|
||||||
|
body = text if text is not None else f"第{chapter}章历史正文"
|
||||||
|
return {
|
||||||
|
"chapter": chapter,
|
||||||
|
"chapter_id": 10000 + chapter,
|
||||||
|
"chapter_status": "published",
|
||||||
|
"block_id": 20000 + chapter,
|
||||||
|
"block_order": 1,
|
||||||
|
"revision": 1,
|
||||||
|
"content_text": body,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _assembly_rows(base_config: dict[str, object]) -> dict[str, object]:
|
||||||
|
"""按预注册 Han 计数构造可通过真实 dry-run 的纯数据快照。"""
|
||||||
|
|
||||||
|
recent_counts: dict[int, int] = {}
|
||||||
|
target_scaffolds: list[dict[str, object]] = []
|
||||||
|
for sample in base_config["samples"]:
|
||||||
|
for chapter, count in zip(
|
||||||
|
sample["targetLengthBasis"]["sourceChapters"],
|
||||||
|
sample["frozenRecentHanCounts"],
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
recent_counts[chapter] = count
|
||||||
|
target = sample["targetChapter"]
|
||||||
|
target_scaffolds.append(
|
||||||
|
{
|
||||||
|
"id": 30000 + target,
|
||||||
|
"chapter": target,
|
||||||
|
"title": f"第{target}章目标",
|
||||||
|
"outline_text": f"第{target}章合法目标细纲,完成预注册场景。",
|
||||||
|
"entities": [],
|
||||||
|
"pattern_hints": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
extra_card_chapters = {188, 189, 190, 317, 460, 461, 462, 487, 540, 541, 542}
|
||||||
|
proxy_names = {
|
||||||
|
188: "联赛系统",
|
||||||
|
189: "联赛系统",
|
||||||
|
190: "赛莉丝",
|
||||||
|
317: "莫妮卡",
|
||||||
|
460: "伊蕾莉雅",
|
||||||
|
461: "伊蕾莉雅",
|
||||||
|
462: "伊蕾莉雅",
|
||||||
|
487: "圣蒂曼行星",
|
||||||
|
540: "迷途之地",
|
||||||
|
541: "迷途之地",
|
||||||
|
542: "迷途之地",
|
||||||
|
}
|
||||||
|
block_rows = []
|
||||||
|
for chapter, count in recent_counts.items():
|
||||||
|
prefix = proxy_names.get(chapter, "")
|
||||||
|
block_rows.append(_block_row(chapter, prefix + "文" * (count - loader.han_count(prefix))))
|
||||||
|
block_rows.extend(
|
||||||
|
_block_row(chapter, f"第{chapter}章{proxy_names[chapter]}历史正文")
|
||||||
|
for chapter in sorted(extra_card_chapters - set(recent_counts))
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"work": {"id": 8, "title": "深空之影", "chapter_count": 594},
|
||||||
|
"reference": {"id": 8, "imported_chapter_count": 594},
|
||||||
|
"source": {
|
||||||
|
"sourceHash": SOURCE_HASH,
|
||||||
|
"sourceVersion": SOURCE_VERSION,
|
||||||
|
"fileName": "深空之影_远瞳.txt",
|
||||||
|
},
|
||||||
|
"authorization": _authorization(),
|
||||||
|
"target_scaffolds": target_scaffolds,
|
||||||
|
"card_rows": _selected_cards(),
|
||||||
|
"block_rows": block_rows,
|
||||||
|
"canonical_character_mentions": [
|
||||||
|
{
|
||||||
|
"sample_id": "deep-space-489-battle",
|
||||||
|
"name": "加特朗",
|
||||||
|
"as_of_chapter": 488,
|
||||||
|
"first_chapter": None,
|
||||||
|
"hit_chapters": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sample_id": "deep-space-321-character-dialogue",
|
||||||
|
"name": "莫妮卡",
|
||||||
|
"as_of_chapter": 320,
|
||||||
|
"first_chapter": 317,
|
||||||
|
"hit_chapters": [317],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sample_id": "deep-space-199-information-reveal",
|
||||||
|
"name": "赛莉丝",
|
||||||
|
"as_of_chapter": 198,
|
||||||
|
"first_chapter": 190,
|
||||||
|
"hit_chapters": [190],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sample_id": "deep-space-523-returning-character",
|
||||||
|
"name": "伊蕾莉雅",
|
||||||
|
"as_of_chapter": 522,
|
||||||
|
"first_chapter": 460,
|
||||||
|
"hit_chapters": [460, 461, 462],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeQueryResult:
|
||||||
|
"""提供 psycopg 结果对象的最小 fetch 接口。"""
|
||||||
|
|
||||||
|
def __init__(self, rows: object):
|
||||||
|
self.rows = rows if isinstance(rows, list) else [rows]
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
"""返回第一行或空值。"""
|
||||||
|
|
||||||
|
return self.rows[0] if self.rows else None
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
"""返回全部测试行。"""
|
||||||
|
|
||||||
|
return self.rows
|
||||||
|
|
||||||
|
|
||||||
|
class FakeReadOnlyConnection:
|
||||||
|
"""记录全部 SQL,证明所有读取都处在同一只读事务。"""
|
||||||
|
|
||||||
|
def __init__(self, rows: dict[str, object]):
|
||||||
|
self.rows = rows
|
||||||
|
self.queries: list[str] = []
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""模拟 psycopg 连接上下文。"""
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
"""测试连接不吞掉异常。"""
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute(self, query, params=None):
|
||||||
|
"""按表名路由固定结果,并保留事务语句供断言。"""
|
||||||
|
|
||||||
|
del params
|
||||||
|
sql = " ".join(str(query).split())
|
||||||
|
self.queries.append(sql)
|
||||||
|
if sql.startswith("SET TRANSACTION"):
|
||||||
|
return FakeQueryResult([])
|
||||||
|
if "FROM muse_content_work" in sql:
|
||||||
|
return FakeQueryResult(self.rows["work"])
|
||||||
|
if "FROM example_reference_work" in sql:
|
||||||
|
return FakeQueryResult([{**self.rows["reference"], "source_file": "深空之影_远瞳.txt", "deleted": False}])
|
||||||
|
if "FROM muse_content_import_task" in sql:
|
||||||
|
return FakeQueryResult(
|
||||||
|
[{
|
||||||
|
"id": 11,
|
||||||
|
"status": "succeeded",
|
||||||
|
"command_id": f"import-{FILE_HASH[:16]}",
|
||||||
|
"source_snapshot": {"file": "深空之影_远瞳.txt"},
|
||||||
|
"deleted": False,
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
if "FROM muse_knowledge_document" in sql:
|
||||||
|
return FakeQueryResult(
|
||||||
|
[{"id": 12, "file_name": "深空之影_远瞳.txt", "file_hash": FILE_HASH, "deleted": False}]
|
||||||
|
)
|
||||||
|
if "FROM example_reference_authorization_snapshot" in sql:
|
||||||
|
snapshot = self.rows["authorization"]["authorizationSnapshot"]
|
||||||
|
return FakeQueryResult(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"snapshot_version": snapshot["version"],
|
||||||
|
"source_hash": SOURCE_HASH,
|
||||||
|
"source_version": SOURCE_VERSION,
|
||||||
|
"copyright_status": "research_only",
|
||||||
|
"source_status": "active",
|
||||||
|
"allowed_purpose": ["offline_evaluation"],
|
||||||
|
"forbidden_purpose": ["external_distribution"],
|
||||||
|
"authorization_basis": "user_authorization",
|
||||||
|
"authorized_by": "user:1",
|
||||||
|
"display_summary": "仅供离线评测",
|
||||||
|
"checked_at": snapshot["checkedAt"],
|
||||||
|
"expires_at": None,
|
||||||
|
"revalidation_at": snapshot["revalidationAt"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if "FROM example_parse_scaffold" in sql:
|
||||||
|
return FakeQueryResult(self.rows["target_scaffolds"])
|
||||||
|
if "FROM muse_knowledge_draft" in sql:
|
||||||
|
return FakeQueryResult(self.rows["card_rows"])
|
||||||
|
if "WITH probes AS" in sql:
|
||||||
|
return FakeQueryResult(self.rows["canonical_character_mentions"])
|
||||||
|
if "FROM muse_content_chapter ch" in sql and "muse_content_block" in sql:
|
||||||
|
return FakeQueryResult(self.rows["block_rows"])
|
||||||
|
raise AssertionError(f"未处理 SQL: {sql}")
|
||||||
|
|
||||||
|
|
||||||
|
class LoadWriterReferenceWorkTest(unittest.TestCase):
|
||||||
|
"""覆盖选择、冻结、事务和真实 Writer dry-run 合同。"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""每个测试都从仓内预注册输入与稳定选择器开始。"""
|
||||||
|
|
||||||
|
self.base_config = json.loads(BASE_CONFIG_PATH.read_text(encoding="utf-8"))
|
||||||
|
self.selectors = json.loads(SELECTOR_PATH.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
def test_resolves_exact_canonical_name_and_alias(self):
|
||||||
|
"""canonical name 与精确 alias 都必须唯一解析,不能模糊挑卡。"""
|
||||||
|
|
||||||
|
resolved = resolve_card_selectors(_selected_cards(), self.selectors)
|
||||||
|
self.assertEqual(resolved["deep-space-489-battle"][0]["id"], 101)
|
||||||
|
self.assertEqual(resolved["deep-space-199-information-reveal"][0]["id"], 104)
|
||||||
|
self.assertEqual(
|
||||||
|
resolved["deep-space-199-information-reveal"][0]["draft_payload"]["名称"],
|
||||||
|
"璐茜",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_duplicate_or_missing_selector_match_fails_closed(self):
|
||||||
|
"""同一选择器命中零张或多张卡时都不能猜测。"""
|
||||||
|
|
||||||
|
duplicate = _selected_cards() + [_card_row(999, "character", "莫妮卡")]
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
resolve_card_selectors(duplicate, self.selectors)
|
||||||
|
missing = [row for row in _selected_cards() if row["id"] != 106]
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
resolve_card_selectors(missing, self.selectors)
|
||||||
|
|
||||||
|
def test_selector_hash_version_and_frozen_controls_fail_closed_on_tamper(self):
|
||||||
|
"""选择器内容、版本、scaffold 来源和上下文上限都必须与 base 公共控制一致。"""
|
||||||
|
|
||||||
|
self.assertEqual(self.base_config["commonControls"]["selectorSha256"], SELECTOR_DIGEST)
|
||||||
|
tampered = json.loads(json.dumps(self.selectors, ensure_ascii=False))
|
||||||
|
tampered["samples"][0]["cards"][0]["name"] = "被篡改"
|
||||||
|
with self.assertRaisesRegex(WriterReferenceWorkError, "SHA-256"):
|
||||||
|
assemble_writer_gate_config(
|
||||||
|
base_config=self.base_config,
|
||||||
|
selector_config=tampered,
|
||||||
|
selector_digest=loader.selector_sha256(
|
||||||
|
json.dumps(tampered, ensure_ascii=False).encode("utf-8")
|
||||||
|
),
|
||||||
|
rows=_assembly_rows(self.base_config),
|
||||||
|
)
|
||||||
|
|
||||||
|
invalid = json.loads(json.dumps(self.base_config, ensure_ascii=False))
|
||||||
|
invalid["commonControls"]["inputProvenance"] = "runtime_scaffold"
|
||||||
|
with self.assertRaisesRegex(WriterReferenceWorkError, "inputProvenance"):
|
||||||
|
assemble_writer_gate_config(
|
||||||
|
base_config=invalid,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=_assembly_rows(self.base_config),
|
||||||
|
)
|
||||||
|
|
||||||
|
invalid = json.loads(json.dumps(self.base_config, ensure_ascii=False))
|
||||||
|
invalid["samples"][0]["writerContextInput"]["tokenBudget"]["maxContextChars"] += 1
|
||||||
|
with self.assertRaisesRegex(WriterReferenceWorkError, "原样使用"):
|
||||||
|
assemble_writer_gate_config(
|
||||||
|
base_config=invalid,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=_assembly_rows(self.base_config),
|
||||||
|
)
|
||||||
|
|
||||||
|
synchronized_tamper = json.loads(json.dumps(self.base_config, ensure_ascii=False))
|
||||||
|
synchronized_tamper["commonControls"]["maxContextChars"] = 150000
|
||||||
|
for sample in synchronized_tamper["samples"]:
|
||||||
|
sample["writerContextInput"]["tokenBudget"]["maxContextChars"] = 150000
|
||||||
|
with self.assertRaisesRegex(WriterReferenceWorkError, "严格等于预注册固定值 140000"):
|
||||||
|
assemble_writer_gate_config(
|
||||||
|
base_config=synchronized_tamper,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=_assembly_rows(self.base_config),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_milestone_ranges_expand_and_keep_latest_three_unique_chapters(self):
|
||||||
|
"""明确区间按绝对章展开,重复章去重后只保留最近三章。"""
|
||||||
|
|
||||||
|
chapters = milestone_reference_chapters(
|
||||||
|
[
|
||||||
|
{"章": 3, "台阶": "早期"},
|
||||||
|
{"章": "5-8", "台阶": "区间"},
|
||||||
|
{"章": 8, "台阶": "重复"},
|
||||||
|
],
|
||||||
|
as_of=8,
|
||||||
|
)
|
||||||
|
self.assertEqual(chapters, [6, 7, 8])
|
||||||
|
|
||||||
|
def test_future_block_or_non_continuous_recent_chapters_fail_closed(self):
|
||||||
|
"""目标章正文和缺章近章都不能进入装配结果。"""
|
||||||
|
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
loader.index_unique_canonical_blocks([_block_row(9)], allowed_chapters={8})
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
loader.select_recent_canonical_blocks(
|
||||||
|
[_block_row(5), _block_row(6), _block_row(8)],
|
||||||
|
as_of=8,
|
||||||
|
)
|
||||||
|
repository = loader.SnapshotProseRepository({8: _block_row(8)})
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
repository.read_source_refs(
|
||||||
|
work_id=8,
|
||||||
|
as_of=8,
|
||||||
|
source_refs=[
|
||||||
|
{
|
||||||
|
"sourceId": "chapter:9:block:20009",
|
||||||
|
"sourceVersion": "chapter:9:block:20009:revision:1",
|
||||||
|
"chapter": 9,
|
||||||
|
"blockId": 20009,
|
||||||
|
"startCodePoint": 0,
|
||||||
|
"endCodePoint": 1,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_han_count_drift_blocks_assembly(self):
|
||||||
|
"""真实近章 Han 计数只要一章漂移就必须阻断。"""
|
||||||
|
|
||||||
|
rows = _assembly_rows(self.base_config)
|
||||||
|
rows["block_rows"][0]["content_text"] += "文"
|
||||||
|
with self.assertRaisesRegex(WriterReferenceWorkError, "Han 计数漂移"):
|
||||||
|
assemble_writer_gate_config(
|
||||||
|
base_config=self.base_config,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_all_database_reads_share_one_repeatable_read_only_transaction(self):
|
||||||
|
"""来源、授权、scaffold、卡与正文必须由同一连接快照读取。"""
|
||||||
|
|
||||||
|
rows = _assembly_rows(self.base_config)
|
||||||
|
connection = FakeReadOnlyConnection(rows)
|
||||||
|
with patch.object(loader.psycopg, "connect", return_value=connection) as connect:
|
||||||
|
loaded = load_writer_reference_rows(
|
||||||
|
dsn="postgresql://unused",
|
||||||
|
tenant_id=1,
|
||||||
|
work_id=8,
|
||||||
|
targets=[199, 321, 489, 523, 544],
|
||||||
|
base_config=self.base_config,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
)
|
||||||
|
connect.assert_called_once()
|
||||||
|
self.assertEqual(
|
||||||
|
connection.queries.count("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY"),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(loaded["target_scaffolds"]), 5)
|
||||||
|
self.assertEqual(len(loaded["card_rows"]), 6)
|
||||||
|
mentions = {item["name"]: item for item in loaded["canonical_character_mentions"]}
|
||||||
|
self.assertEqual(mentions["加特朗"]["hit_chapters"], [])
|
||||||
|
self.assertEqual(mentions["莫妮卡"]["first_chapter"], 317)
|
||||||
|
|
||||||
|
def test_complete_real_content_config_passes_writer_dry_run(self):
|
||||||
|
"""纯数据装配出的 canonical_frozen_prose 五样本配置通过真实 dry-run。"""
|
||||||
|
|
||||||
|
config = assemble_writer_gate_config(
|
||||||
|
base_config=self.base_config,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=_assembly_rows(self.base_config),
|
||||||
|
)
|
||||||
|
self.assertEqual(len(config["samples"]), 5)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
sample["writerContextInput"]["contentMode"] == "canonical_frozen_prose"
|
||||||
|
for sample in config["samples"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for sample in config["samples"]:
|
||||||
|
context = sample["writerContextInput"]
|
||||||
|
retrieval = context["retrievalResult"]
|
||||||
|
self.assertEqual(retrieval["factEvidence"], [])
|
||||||
|
self.assertEqual(context["tokenBudget"]["maxContextChars"], 140000)
|
||||||
|
self.assertTrue(all(len(card["sourceRefs"]) <= 3 for card in retrieval["cards"]))
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
ref["sourceType"] == "card_chapter_proxy"
|
||||||
|
for card in retrieval["cards"]
|
||||||
|
for ref in card["sourceRefs"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
evidence["purpose"] == "card_chapter_proxy"
|
||||||
|
for evidence in retrieval["proseEvidence"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
evidence["chapter"] <= sample["asOfChapter"]
|
||||||
|
for evidence in [*context["recentChapters"], *retrieval["proseEvidence"]]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = run_writer_replay(config, run_id="writer-loader-test")
|
||||||
|
self.assertTrue(result["ok"], result)
|
||||||
|
self.assertEqual(result["status"], "ready")
|
||||||
|
|
||||||
|
def test_character_ratio_comes_from_canonical_text_not_contaminated_card(self):
|
||||||
|
"""卡内塞入角色名不能把 Canonical 正文从未出现的角色伪装成已知。"""
|
||||||
|
|
||||||
|
rows = _assembly_rows(self.base_config)
|
||||||
|
rows["card_rows"][0]["draft_payload"]["别名"].append("加特朗")
|
||||||
|
config = assemble_writer_gate_config(
|
||||||
|
base_config=self.base_config,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=rows,
|
||||||
|
)
|
||||||
|
samples = {item["targetChapter"]: item for item in config["samples"]}
|
||||||
|
self.assertEqual(samples[489]["newCharacterBasis"]["absentBeforeAsOf"], ["加特朗"])
|
||||||
|
self.assertEqual(samples[489]["newCharacterRatio"], 1.0)
|
||||||
|
self.assertEqual(samples[321]["newCharacterBasis"]["knownBeforeAsOf"], ["莫妮卡"])
|
||||||
|
|
||||||
|
def test_derived_chapter_proxy_requires_name_or_preregistered_alias_in_each_chapter(self):
|
||||||
|
"""整章代理若不能在对应正文命中规范名或预注册别名,必须失败关闭。"""
|
||||||
|
|
||||||
|
rows = _assembly_rows(self.base_config)
|
||||||
|
target = next(row for row in rows["block_rows"] if row["chapter"] == 188)
|
||||||
|
target["content_text"] = "第188章无关历史正文"
|
||||||
|
with self.assertRaisesRegex(WriterReferenceWorkError, "规范名或合法别名"):
|
||||||
|
assemble_writer_gate_config(
|
||||||
|
base_config=self.base_config,
|
||||||
|
selector_config=self.selectors,
|
||||||
|
selector_digest=SELECTOR_DIGEST,
|
||||||
|
rows=rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_output_must_be_new_private_tmp_child(self):
|
||||||
|
"""配置只能写入 /private/tmp 的全新独立子目录。"""
|
||||||
|
|
||||||
|
config = {"profile": "writer_replay"}
|
||||||
|
with tempfile.TemporaryDirectory(dir="/private/tmp") as parent:
|
||||||
|
output_dir = pathlib.Path(parent) / "writer-config"
|
||||||
|
path = write_temporary_config(config, output_dir)
|
||||||
|
self.assertEqual(path, output_dir / "config.json")
|
||||||
|
self.assertEqual(json.loads(path.read_text(encoding="utf-8")), config)
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
write_temporary_config(config, output_dir)
|
||||||
|
with self.assertRaises(WriterReferenceWorkError):
|
||||||
|
write_temporary_config(config, SCRIPT_DIR / "forbidden-output")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -547,6 +547,15 @@ class WriterReplayDryRunTest(unittest.TestCase):
|
|||||||
gate_config = json.loads(gate_config_path.read_text(encoding="utf-8"))
|
gate_config = json.loads(gate_config_path.read_text(encoding="utf-8"))
|
||||||
expected_targets = {489: 7500, 321: 7600, 544: 6700, 199: 2000, 523: 6100}
|
expected_targets = {489: 7500, 321: 7600, 544: 6700, 199: 2000, 523: 6100}
|
||||||
expected_ratios = {489: 1.0, 321: 0.0, 544: None, 199: 0.0, 523: 0.0}
|
expected_ratios = {489: 1.0, 321: 0.0, 544: None, 199: 0.0, 523: 0.0}
|
||||||
|
self.assertEqual(
|
||||||
|
gate_config["commonControls"]["selectorSha256"],
|
||||||
|
"sha256:9df06f7d5864696deda3dd119f8cbded01ba9a9c7e5bf5655553df938f267691",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
gate_config["commonControls"]["inputProvenance"],
|
||||||
|
"oracle_reference_scaffold",
|
||||||
|
)
|
||||||
|
self.assertEqual(gate_config["commonControls"]["maxContextChars"], 140000)
|
||||||
|
|
||||||
for sample in gate_config["samples"]:
|
for sample in gate_config["samples"]:
|
||||||
basis = sample["targetLengthBasis"]
|
basis = sample["targetLengthBasis"]
|
||||||
@ -562,6 +571,10 @@ class WriterReplayDryRunTest(unittest.TestCase):
|
|||||||
self.assertEqual(recalculated, expected_targets[target_chapter])
|
self.assertEqual(recalculated, expected_targets[target_chapter])
|
||||||
self.assertEqual(sample["targetChars"], recalculated)
|
self.assertEqual(sample["targetChars"], recalculated)
|
||||||
self.assertEqual(sample["newCharacterRatio"], expected_ratios[target_chapter])
|
self.assertEqual(sample["newCharacterRatio"], expected_ratios[target_chapter])
|
||||||
|
self.assertEqual(
|
||||||
|
sample["writerContextInput"]["tokenBudget"]["maxContextChars"],
|
||||||
|
gate_config["commonControls"]["maxContextChars"],
|
||||||
|
)
|
||||||
if target_chapter == 544:
|
if target_chapter == 544:
|
||||||
self.assertEqual(sample["newCharacterRatioStatus"], "unresolved_generic_role")
|
self.assertEqual(sample["newCharacterRatioStatus"], "unresolved_generic_role")
|
||||||
else:
|
else:
|
||||||
@ -686,10 +699,21 @@ class WriterReplayExecuteBoundaryTest(unittest.TestCase):
|
|||||||
detector = FakeSemanticDetector()
|
detector = FakeSemanticDetector()
|
||||||
judge = MaliciousJudge()
|
judge = MaliciousJudge()
|
||||||
adapters = WriterReplayTestAdapters(runner, detector, judge)
|
adapters = WriterReplayTestAdapters(runner, detector, judge)
|
||||||
|
evaluation_config = config()
|
||||||
|
fine_outline = evaluation_config["samples"][0]["writerContextInput"]["fineOutline"]
|
||||||
|
# 恶意配置模拟把目标原文、索引、文件路径和真实臂塞进细纲对象;这些字段写手看不到,评委也不能看到。
|
||||||
|
fine_outline.update(
|
||||||
|
{
|
||||||
|
"targetOriginal": "目标章原文泄漏哨兵",
|
||||||
|
"indexHints": [{"content": "被测卡索引泄漏哨兵"}],
|
||||||
|
"path": "/private/tmp/candidate-A.json",
|
||||||
|
"arm": "A",
|
||||||
|
}
|
||||||
|
)
|
||||||
with tempfile.TemporaryDirectory(dir="/private/tmp") as directory:
|
with tempfile.TemporaryDirectory(dir="/private/tmp") as directory:
|
||||||
output_dir = pathlib.Path(directory) / "run"
|
output_dir = pathlib.Path(directory) / "run"
|
||||||
result = run_writer_replay(
|
result = run_writer_replay(
|
||||||
config(),
|
evaluation_config,
|
||||||
run_id="blind-isolation",
|
run_id="blind-isolation",
|
||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
execute=True,
|
execute=True,
|
||||||
@ -698,6 +722,20 @@ class WriterReplayExecuteBoundaryTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(result["ok"])
|
self.assertTrue(result["ok"])
|
||||||
self.assertTrue(judge.visible_inputs)
|
self.assertTrue(judge.visible_inputs)
|
||||||
|
expected_fine_outline = {
|
||||||
|
"sourceRef": {
|
||||||
|
"sourceId": "fixture:fine-outline:489",
|
||||||
|
"sourceVersion": "fine-outline-fixture-v1",
|
||||||
|
"chapter": 489,
|
||||||
|
},
|
||||||
|
"hardConstraints": ["必须完成围攻突围"],
|
||||||
|
"adjustableBeats": [],
|
||||||
|
"declaredNewFacts": [],
|
||||||
|
}
|
||||||
|
self.assertTrue(
|
||||||
|
all(context["fineOutline"] == expected_fine_outline for context in runner.contexts),
|
||||||
|
"三臂写手必须只看到严格细纲合同字段",
|
||||||
|
)
|
||||||
shared_references = []
|
shared_references = []
|
||||||
for visible in judge.visible_inputs:
|
for visible in judge.visible_inputs:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -714,7 +752,8 @@ class WriterReplayExecuteBoundaryTest(unittest.TestCase):
|
|||||||
self.assertEqual(set(visible["candidateOrder"]), {"blind-1", "blind-2", "blind-3"})
|
self.assertEqual(set(visible["candidateOrder"]), {"blind-1", "blind-2", "blind-3"})
|
||||||
shared = visible["sharedEvaluationReference"]
|
shared = visible["sharedEvaluationReference"]
|
||||||
shared_references.append(copy.deepcopy(shared))
|
shared_references.append(copy.deepcopy(shared))
|
||||||
self.assertEqual(shared["fineOutline"]["hardConstraints"], ["必须完成围攻突围"])
|
self.assertEqual(shared["fineOutline"], expected_fine_outline)
|
||||||
|
self.assertNotIn("entities", shared["fineOutline"])
|
||||||
self.assertEqual(shared["requirements"]["requiredCharacters"], ["林澈"])
|
self.assertEqual(shared["requirements"]["requiredCharacters"], ["林澈"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
shared["requirements"]["chapterEndHook"]["anchors"],
|
shared["requirements"]["chapterEndHook"]["anchors"],
|
||||||
|
|||||||
@ -136,6 +136,8 @@ writer 可以创造非 Canonical 的环境细节、动作、过渡、无名配
|
|||||||
|
|
||||||
正向创作没有回放冻结点时,`asOf` 等于当前 Canonical 最新章。
|
正向创作没有回放冻结点时,`asOf` 等于当前 Canonical 最新章。
|
||||||
|
|
||||||
|
离线 eval_draft 的 `indexHint.content` 只允许保存卡名、类型和实际回读章号,不得透传里程碑台阶。这个收紧只作用于诊断评测卡,生产 Canonical 卡继续使用既有冻结状态提示。
|
||||||
|
|
||||||
### 5.3 根据卡回读原文
|
### 5.3 根据卡回读原文
|
||||||
|
|
||||||
首版必须保留 run3 已验证条件:**目标章之前连续 4 章全文**。选择性裁剪只能作为后续 A/B 实验变量;未证明不降质前,不得替代四章全文基线。
|
首版必须保留 run3 已验证条件:**目标章之前连续 4 章全文**。选择性裁剪只能作为后续 A/B 实验变量;未证明不降质前,不得替代四章全文基线。
|
||||||
@ -147,6 +149,8 @@ writer 可以创造非 Canonical 的环境细节、动作、过渡、无名配
|
|||||||
3. 能力、物品或关系的代表性表现;
|
3. 能力、物品或关系的代表性表现;
|
||||||
4. 与本章细纲同类的历史场景。
|
4. 与本章细纲同类的历史场景。
|
||||||
|
|
||||||
|
若 eval_draft 卡没有持久化 `sourceRefs`,Gate A 可把冻结线内最近三个里程碑章的整章 Canonical block 作为降级代理。该来源必须同时标记 `sourceRef.sourceType=card_chapter_proxy` 和 `proseEvidence.purpose=card_chapter_proxy`,不能伪装成精确片段;每个代理章正文必须出现卡规范名或预注册合法别名,否则失败关闭。连续四章基线不可裁剪,代理等弱补充由 assembler 在固定预算内裁剪。
|
||||||
|
|
||||||
`RetrievalManifest` 保存 `planId`、查询、过滤、排序、来源版本、章号、片段 offset、内容哈希和裁剪原因。`manifestId` 对规范化来源集合计算 SHA-256,排除 `runId`、时间戳和执行节点;同一 `planId` 与索引版本必须产生相同 `manifestId`。
|
`RetrievalManifest` 保存 `planId`、查询、过滤、排序、来源版本、章号、片段 offset、内容哈希和裁剪原因。`manifestId` 对规范化来源集合计算 SHA-256,排除 `runId`、时间戳和执行节点;同一 `planId` 与索引版本必须产生相同 `manifestId`。
|
||||||
|
|
||||||
## 6. 双证据模型
|
## 6. 双证据模型
|
||||||
@ -206,6 +210,8 @@ writer 可以创造非 Canonical 的环境细节、动作、过渡、无名配
|
|||||||
|
|
||||||
所有对象使用严格 schema,额外字段失败;引用的 ID、版本和 hash 必须存在且一致。
|
所有对象使用严格 schema,额外字段失败;引用的 ID、版本和 hash 必须存在且一致。
|
||||||
|
|
||||||
|
Gate A 的公共控制还固定 `selectorVersion`、选择器规范 JSON 原始字节 SHA-256、`inputProvenance=oracle_reference_scaffold` 和 `maxContextChars=140000`。loader 在任何数据库读取前核对并原样沿用这些值,不得按真实输入扩大预算;若固定预算连细纲硬约束和连续四章全文基线都装不下,assembler 必须失败关闭。
|
||||||
|
|
||||||
`evidenceStrategy` 是兼容字段:生产上下文未填写时按 `production_dual_evidence` 校验;正文 A/B/C 回放不得使用缺省值,必须分别显式填写 `historical_prose_only`、`card_index_only`、`card_index_plus_prose`。`indexHints` 只能在 `mode=diagnostic_only`、`purpose=evaluation/diagnostic` 且 `acceptanceEligible=false` 时出现;生产上下文硬拒绝。其 `asOf` 不得超过上下文冻结点,`claimLedger` 不得引用 `indexHints`。只有显式填写 `evidenceStrategy=card_index_only` 的 B 臂允许在合同内跳过连续四章基线,并且必须保持 `proseEvidence=[]`;该例外不能用于生产。
|
`evidenceStrategy` 是兼容字段:生产上下文未填写时按 `production_dual_evidence` 校验;正文 A/B/C 回放不得使用缺省值,必须分别显式填写 `historical_prose_only`、`card_index_only`、`card_index_plus_prose`。`indexHints` 只能在 `mode=diagnostic_only`、`purpose=evaluation/diagnostic` 且 `acceptanceEligible=false` 时出现;生产上下文硬拒绝。其 `asOf` 不得超过上下文冻结点,`claimLedger` 不得引用 `indexHints`。只有显式填写 `evidenceStrategy=card_index_only` 的 B 臂允许在合同内跳过连续四章基线,并且必须保持 `proseEvidence=[]`;该例外不能用于生产。
|
||||||
|
|
||||||
### 7.2 `WriterOutput v1`
|
### 7.2 `WriterOutput v1`
|
||||||
@ -273,7 +279,7 @@ detector 只阻断可定位、可验证的问题:细纲硬约束漏项、事
|
|||||||
- 离线优化允许最多 5 轮,且每轮只改一个变量;与生产两轮返修是两套状态机。
|
- 离线优化允许最多 5 轮,且每轮只改一个变量;与生产两轮返修是两套状态机。
|
||||||
- 三臂使用相同细纲、冻结点、模型、篇幅算法和最大生成预算。
|
- 三臂使用相同细纲、冻结点、模型、篇幅算法和最大生成预算。
|
||||||
- 候选臂名映射为随机化 ID;顺序种子由预注册 `evaluationSetVersion + sampleId` 派生并固定。
|
- 候选臂名映射为随机化 ID;顺序种子由预注册 `evaluationSetVersion + sampleId` 派生并固定。
|
||||||
- judge 的可信输入边界是独立 `writer-blind-input-v1` 内容,包含 `blind-1/2/3` 候选正文/哈希,以及所有臂完全相同的 `sharedEvaluationReference`。共同参考只取目标章细纲硬约束、实体、必需角色、伏笔、章末钩子和冻结点前连续四章历史原文基准,用于评价细纲忠实、设定、文风和卡索引正确性;它可在真实运行时临时传递,但不进入安全摘要。共同参考严禁包含 `indexHints`、各臂补充原文、卡 manifest、`evidenceStrategy`、真实映射或臂名,不能用被测卡本身给被测候选背书。真实 A/B/C 映射仅在编排器内存中存在,judge 不得访问各臂 WriterContext、原始运行目录或含 `candidate-A/B/C` 的路径。
|
- judge 的可信输入边界是独立 `writer-blind-input-v1` 内容,包含 `blind-1/2/3` 候选正文/哈希,以及所有臂完全相同的 `sharedEvaluationReference`。共同参考的细纲只保留 `sourceRef/hardConstraints/adjustableBeats/declaredNewFacts` 四字段,不增加 `entities`;另带必需角色、伏笔、章末钩子和冻结点前连续四章历史原文基准,用于评价细纲忠实、设定、文风和卡索引正确性。它可在真实运行时临时传递,但不进入安全摘要。共同参考严禁包含 `indexHints`、各臂补充原文、卡 manifest、`evidenceStrategy`、真实映射或臂名,不能用被测卡本身给被测候选背书。真实 A/B/C 映射仅在编排器内存中存在,judge 不得访问各臂 WriterContext、原始运行目录或含 `candidate-A/B/C` 的路径。
|
||||||
- 两个评委使用独立无会话实例,第二评委反转顺序;评分步长为 0.5。
|
- 两个评委使用独立无会话实例,第二评委反转顺序;评分步长为 0.5。
|
||||||
- 同维分差 > 0.5 时判不稳定,最多增加一次第三评委。三评分中若至少一对差值 <=0.5,则该维最终分取三者中位数;若不存在稳定配对,则样本进入 `invalid_unstable`,不参与方向结论。
|
- 同维分差 > 0.5 时判不稳定,最多增加一次第三评委。三评分中若至少一对差值 <=0.5,则该维最终分取三者中位数;若不存在稳定配对,则样本进入 `invalid_unstable`,不参与方向结论。
|
||||||
- 五维:设定与实体保真、情节与细纲忠实、叙事完整与张力、文风一致、文笔质量。
|
- 五维:设定与实体保真、情节与细纲忠实、叙事完整与张力、文风一致、文笔质量。
|
||||||
@ -307,7 +313,9 @@ detector 只阻断可定位、可验证的问题:细纲硬约束漏项、事
|
|||||||
|
|
||||||
预期长度不是人工填写:配置记录目标章之前连续四章的 `frozenRecentHanCounts`,统一以 `hardEventCount=1`、`foreshadowingActionCount=0`、`requiredSceneCount=0` 调用 `calculate_target_chars`,且 `usesTargetChapterLength=false`。五章机械结果固定为 489=7500、321=7600、544=6700、199=2000、523=6100;上下限取正负 10% 后再受 2000-10000 限幅。
|
预期长度不是人工填写:配置记录目标章之前连续四章的 `frozenRecentHanCounts`,统一以 `hardEventCount=1`、`foreshadowingActionCount=0`、`requiredSceneCount=0` 调用 `calculate_target_chars`,且 `usesTargetChapterLength=false`。五章机械结果固定为 489=7500、321=7600、544=6700、199=2000、523=6100;上下限取正负 10% 后再受 2000-10000 限幅。
|
||||||
|
|
||||||
新角色比例只统计具名 `requiredCharacters` 在冻结点前无记录的比例。489 的加特朗在 488 前无记录,因此为 1.0;321/199/523 的具名角色已有记录,因此为 0;544 的“内应”是泛称、无法机械判定具体身份,必须为 `null + unresolved_generic_role`,不允许默认成 0。
|
新角色比例只统计具名 `requiredCharacters` 在冻结点前无记录的比例。真实 loader 在来源、授权、scaffold、卡和正文所在的同一个 `REPEATABLE READ READ ONLY` 事务内,直接扫描冻结历史 Canonical 正文并返回名字对应的首次命中章与命中章集合,再独立重算 `knownBeforeAsOf/absentBeforeAsOf/newCharacterRatio`,不得相信卡内是否出现该名字。489 的加特朗在 488 前无记录,因此为 1.0;321/199/523 的具名角色已有记录,因此为 0;544 的“内应”是泛称、无法机械判定具体身份,必须为 `null + unresolved_generic_role`,不允许默认成 0。
|
||||||
|
|
||||||
|
真实装配保留目标章 reference scaffold,并以 `inputProvenance=oracle_reference_scaffold` 明示来源。它只用于固定本次正文层细纲输入和目标事实泄漏代理;这项验证不覆盖上游清洗、抽卡、范式、细纲生成或完整创作链,不能据此宣称整条创作链完成。
|
||||||
|
|
||||||
仓内配置不含原文全文。`writerContextInput.contentMode=sanitized_contract_fixture`,连续四章只放脱敏合成短文本,用于 dry-run 机械证明 WriterContext 合同、冻结边界、三臂差异、manifest 可复现和候选不可接受;不能据此声称历史原文完整、生成质量通过或 real-run 完成。
|
仓内配置不含原文全文。`writerContextInput.contentMode=sanitized_contract_fixture`,连续四章只放脱敏合成短文本,用于 dry-run 机械证明 WriterContext 合同、冻结边界、三臂差异、manifest 可复现和候选不可接受;不能据此声称历史原文完整、生成质量通过或 real-run 完成。
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user