mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
86bf0e5c2c
Refs #660. Measurement and issue closure follow in the separately preregistered post-main mechanical conformance PR.
2960 lines
105 KiB
Python
2960 lines
105 KiB
Python
"""Hermetic conformance and mutation tests for issue #660."""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import unicodedata
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
from jsonschema import Draft202012Validator
|
|
|
|
|
|
SCRIPTS = Path(__file__).resolve().parent
|
|
REPO_ROOT = SCRIPTS.parent
|
|
FIXTURES = SCRIPTS / "fixtures/tortured_phrase_screening"
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
|
|
import tortured_phrase_screening as screening # noqa: E402
|
|
|
|
|
|
CHECKED_AT = "2026-08-10T01:00:00Z"
|
|
RECORDED_AT = "2026-08-10T01:00:01Z"
|
|
|
|
|
|
def _bundle() -> screening.SnapshotBundle:
|
|
return screening.load_snapshot(
|
|
FIXTURES / "snapshot.json", FIXTURES / "snapshot_manifest.json"
|
|
)
|
|
|
|
|
|
def _state() -> screening.SnapshotState:
|
|
bundle = _bundle()
|
|
return screening.SnapshotState(
|
|
status="loaded",
|
|
reason_code="CHECK_COMPLETED",
|
|
bundle=bundle,
|
|
snapshot_sha256=bundle.snapshot_sha256,
|
|
manifest_sha256=bundle.manifest_sha256,
|
|
detail=None,
|
|
)
|
|
|
|
|
|
def _draft(name: str) -> str:
|
|
return (FIXTURES / name).read_text(encoding="utf-8")
|
|
|
|
|
|
def _report(name: str = "own_draft.md") -> dict:
|
|
return screening.build_own_draft_report(
|
|
_draft(name),
|
|
artifact_id=name,
|
|
artifact_format="latex" if name.endswith(".tex") else "markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def _passport() -> dict:
|
|
return yaml.safe_load((FIXTURES / "corpus_input.yaml").read_text(encoding="utf-8"))
|
|
|
|
|
|
def _enriched(state: screening.SnapshotState | None = None) -> dict:
|
|
return screening.enrich_passport(
|
|
_passport(),
|
|
state=state or _state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def _seed_expectations() -> dict:
|
|
value, _raw = screening._strict_json_path(
|
|
FIXTURES / "seed_expectations.json",
|
|
label="synthetic seed expectations",
|
|
maximum=512 * 1024,
|
|
)
|
|
if not isinstance(value, dict):
|
|
raise AssertionError("seed expectations root must be an object")
|
|
return value
|
|
|
|
|
|
def _expect_exact_keys(
|
|
value: object,
|
|
*,
|
|
required: set[str],
|
|
optional: set[str] | None = None,
|
|
label: str,
|
|
) -> dict:
|
|
if not isinstance(value, dict):
|
|
raise AssertionError(f"{label} must be an object")
|
|
optional = optional or set()
|
|
keys = set(value)
|
|
if keys != required | (keys & optional):
|
|
missing = required - keys
|
|
unknown = keys - required - optional
|
|
raise AssertionError(
|
|
f"{label} keys drifted; missing={sorted(missing)!r}, "
|
|
f"unknown={sorted(unknown)!r}"
|
|
)
|
|
return value
|
|
|
|
|
|
def _compiled_rules() -> dict[str, screening.CompiledRule]:
|
|
return {rule.rule_id: rule for rule in _bundle().rules}
|
|
|
|
|
|
def _actual_suppressions(
|
|
text: str,
|
|
segments: list[screening.Segment],
|
|
rules: dict[str, screening.CompiledRule],
|
|
) -> Counter[tuple[str, str]]:
|
|
suppressed: Counter[tuple[str, str]] = Counter()
|
|
for segment in segments:
|
|
tokens = screening.tokenize(text[segment.start : segment.end])
|
|
for rule in rules.values():
|
|
included = set(screening.evaluate_expression(rule.expression, tokens))
|
|
retained = set(screening.evaluate_rule(rule, tokens))
|
|
suppressed[(rule.rule_id, segment.kind)] += len(included - retained)
|
|
return +suppressed
|
|
|
|
|
|
def _has_token_boundary_negative(
|
|
rule: screening.CompiledRule,
|
|
text: str,
|
|
segments: list[screening.Segment],
|
|
) -> bool:
|
|
expression = rule.expression
|
|
if expression.get("op") != "literal":
|
|
return False
|
|
literal_tokens = screening.tokenize(expression["value"])
|
|
if len(literal_tokens) != 1:
|
|
return False
|
|
literal = literal_tokens[0].normalized
|
|
return any(
|
|
literal in token.normalized and literal != token.normalized
|
|
for segment in segments
|
|
for token in screening.tokenize(text[segment.start : segment.end])
|
|
)
|
|
|
|
|
|
def _has_same_segment_exclusion(
|
|
rule: screening.CompiledRule,
|
|
text: str,
|
|
segments: list[screening.Segment],
|
|
) -> bool:
|
|
if not rule.exclude_if:
|
|
return False
|
|
for segment in segments:
|
|
tokens = screening.tokenize(text[segment.start : segment.end])
|
|
included = set(screening.evaluate_expression(rule.expression, tokens))
|
|
if included and included - set(screening.evaluate_rule(rule, tokens)):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _non_match_reason_holds(
|
|
reason: str,
|
|
rule: screening.CompiledRule,
|
|
text: str,
|
|
segments: list[screening.Segment],
|
|
) -> bool:
|
|
if reason == "token_boundary":
|
|
return _has_token_boundary_negative(rule, text, segments)
|
|
if reason == "exclude_if_in_same_segment":
|
|
return _has_same_segment_exclusion(rule, text, segments)
|
|
raise AssertionError(f"unsupported synthetic non-match reason {reason!r}")
|
|
|
|
|
|
def _normalization_case_holds(
|
|
case: str,
|
|
match: dict,
|
|
rule: screening.CompiledRule,
|
|
) -> bool:
|
|
expression = rule.expression
|
|
if expression.get("op") != "literal":
|
|
return False
|
|
raw = match["matched_text"]
|
|
actual_tokens = tuple(token.normalized for token in screening.tokenize(raw))
|
|
literal_tokens = tuple(
|
|
token.normalized for token in screening.tokenize(expression["value"])
|
|
)
|
|
if actual_tokens != literal_tokens:
|
|
return False
|
|
if case == "nfkc_casefold":
|
|
folded = unicodedata.normalize("NFKC", raw).casefold()
|
|
return folded == expression["value"].casefold() and raw.casefold() != folded
|
|
if case == "same_line_dash_separator":
|
|
return "\n" not in raw and any(char in screening._DASHES for char in raw)
|
|
if case == "soft_hyphen_join":
|
|
return "\u00ad" in raw and "\n" not in raw
|
|
if case == "line_break_hyphen_join":
|
|
return any(
|
|
char in screening._DASHES and index + 1 < len(raw) and raw[index + 1] == "\n"
|
|
for index, char in enumerate(raw)
|
|
)
|
|
raise AssertionError(f"unsupported normalization case {case!r}")
|
|
|
|
|
|
def _alternative_matches_record(
|
|
alternative: dict,
|
|
match: dict,
|
|
*,
|
|
text: str,
|
|
segments: dict[str, screening.Segment],
|
|
) -> bool:
|
|
segment = segments[match["segment_id"]]
|
|
tokens = screening.tokenize(text[segment.start : segment.end])
|
|
span = match["source_span"]
|
|
return any(
|
|
segment.start + witness.codepoint_start == span["codepoint_start"]
|
|
and segment.start + witness.codepoint_end == span["codepoint_end"]
|
|
for witness in screening.evaluate_expression(alternative, tokens)
|
|
)
|
|
|
|
|
|
def _spans_overlap(left: dict, right: dict) -> bool:
|
|
if left["segment_id"] != right["segment_id"]:
|
|
return False
|
|
left_span = left["source_span"]
|
|
right_span = right["source_span"]
|
|
return max(left_span["codepoint_start"], right_span["codepoint_start"]) < min(
|
|
left_span["codepoint_end"], right_span["codepoint_end"]
|
|
)
|
|
|
|
|
|
def _replay_draft_expectation(
|
|
expectation: dict,
|
|
rules: dict[str, screening.CompiledRule],
|
|
) -> set[str]:
|
|
expectation = _expect_exact_keys(
|
|
expectation,
|
|
required={
|
|
"path",
|
|
"expected_counts",
|
|
"expected_matches",
|
|
"expected_suppressions",
|
|
"expected_non_matches",
|
|
},
|
|
label="draft expectation",
|
|
)
|
|
path = expectation["path"]
|
|
if path not in {"own_draft.md", "own_draft.tex"}:
|
|
raise AssertionError(f"unsupported draft expectation path {path!r}")
|
|
text = _draft(path)
|
|
artifact_format = "latex" if path.endswith(".tex") else "markdown"
|
|
segments = screening.segment_document(text, artifact_format)
|
|
segments_by_id = {segment.segment_id: segment for segment in segments}
|
|
report = _report(path)
|
|
if report["evaluation_status"] != "UNMEASURED":
|
|
raise AssertionError(f"{path}: synthetic results must remain UNMEASURED")
|
|
|
|
expected_counts = _expect_exact_keys(
|
|
expectation["expected_counts"],
|
|
required={
|
|
"matched_rule_count",
|
|
"rule_match_count",
|
|
"unique_instance_count",
|
|
},
|
|
label=f"{path}.expected_counts",
|
|
)
|
|
for key, expected in expected_counts.items():
|
|
if type(expected) is not int or expected < 0:
|
|
raise AssertionError(f"{path}.expected_counts.{key} must be nonnegative int")
|
|
if report["counts"][key] != expected:
|
|
raise AssertionError(
|
|
f"{path}.{key}: expected {expected}, got {report['counts'][key]}"
|
|
)
|
|
|
|
expected_match_counts: Counter[tuple[str, str]] = Counter()
|
|
seen_match_keys: set[tuple[str, str]] = set()
|
|
match_groups: dict[str, list[dict]] = {}
|
|
group_by_match_id: dict[str, str | None] = {}
|
|
normalization_cases: set[str] = set()
|
|
for index, raw_expected in enumerate(expectation["expected_matches"]):
|
|
expected = _expect_exact_keys(
|
|
raw_expected,
|
|
required={"rule_id", "count", "context"},
|
|
optional={
|
|
"matched_alternative_index",
|
|
"overlap_group",
|
|
"normalization_case",
|
|
},
|
|
label=f"{path}.expected_matches[{index}]",
|
|
)
|
|
rule_id = expected["rule_id"]
|
|
context = expected["context"]
|
|
count = expected["count"]
|
|
if rule_id not in rules:
|
|
raise AssertionError(f"{path}: unknown expected rule {rule_id!r}")
|
|
if context not in screening.CONTEXTS:
|
|
raise AssertionError(f"{path}: unknown expected context {context!r}")
|
|
if type(count) is not int or count < 1:
|
|
raise AssertionError(f"{path}: expected match count must be positive int")
|
|
match_key = (rule_id, context)
|
|
if match_key in seen_match_keys:
|
|
raise AssertionError(f"{path}: duplicate expected match key {match_key!r}")
|
|
seen_match_keys.add(match_key)
|
|
expected_match_counts[match_key] = count
|
|
actual_matches = [
|
|
match
|
|
for match in report["matches"]
|
|
if (match["pattern_id"], match["context"]) == match_key
|
|
]
|
|
|
|
expression = rules[rule_id].expression
|
|
alternative_index = expected.get("matched_alternative_index")
|
|
if expression.get("op") == "any":
|
|
if type(alternative_index) is not int:
|
|
raise AssertionError(f"{path}: any rule {rule_id!r} needs an alternative index")
|
|
alternatives = expression["alternatives"]
|
|
if not 0 <= alternative_index < len(alternatives):
|
|
raise AssertionError(f"{path}: alternative index out of range")
|
|
for match in actual_matches:
|
|
matching_indexes = {
|
|
child_index
|
|
for child_index, alternative in enumerate(alternatives)
|
|
if _alternative_matches_record(
|
|
alternative,
|
|
match,
|
|
text=text,
|
|
segments=segments_by_id,
|
|
)
|
|
}
|
|
if matching_indexes != {alternative_index}:
|
|
raise AssertionError(
|
|
f"{path}: any alternative mismatch for {rule_id!r}: "
|
|
f"{sorted(matching_indexes)!r}"
|
|
)
|
|
elif alternative_index is not None:
|
|
raise AssertionError(f"{path}: non-any rule carries alternative index")
|
|
|
|
overlap_group = expected.get("overlap_group")
|
|
if overlap_group is not None and (
|
|
not isinstance(overlap_group, str) or not overlap_group
|
|
):
|
|
raise AssertionError(f"{path}: overlap_group must be a nonempty string")
|
|
for match in actual_matches:
|
|
group_by_match_id[match["match_id"]] = overlap_group
|
|
if overlap_group is not None:
|
|
match_groups.setdefault(overlap_group, []).append(match)
|
|
|
|
normalization_case = expected.get("normalization_case")
|
|
if normalization_case is not None:
|
|
if not isinstance(normalization_case, str):
|
|
raise AssertionError(f"{path}: normalization_case must be a string")
|
|
normalization_cases.add(normalization_case)
|
|
if not actual_matches or not all(
|
|
_normalization_case_holds(normalization_case, match, rules[rule_id])
|
|
for match in actual_matches
|
|
):
|
|
raise AssertionError(
|
|
f"{path}: normalization case {normalization_case!r} did not replay"
|
|
)
|
|
|
|
actual_match_counts = Counter(
|
|
(match["pattern_id"], match["context"]) for match in report["matches"]
|
|
)
|
|
if actual_match_counts != expected_match_counts:
|
|
raise AssertionError(
|
|
f"{path}: expected rule/context counts do not match runtime output"
|
|
)
|
|
|
|
for group, matches in match_groups.items():
|
|
if len(matches) < 2 or screening._unique_instance_count(matches) != 1:
|
|
raise AssertionError(f"{path}: overlap group {group!r} is not one component")
|
|
for left_index, left in enumerate(report["matches"]):
|
|
for right in report["matches"][left_index + 1 :]:
|
|
if _spans_overlap(left, right) and (
|
|
group_by_match_id.get(left["match_id"]) is None
|
|
or group_by_match_id.get(left["match_id"])
|
|
!= group_by_match_id.get(right["match_id"])
|
|
):
|
|
raise AssertionError(f"{path}: overlapping matches lack one shared group")
|
|
|
|
expected_suppressions: Counter[tuple[str, str]] = Counter()
|
|
for index, raw_expected in enumerate(expectation["expected_suppressions"]):
|
|
expected = _expect_exact_keys(
|
|
raw_expected,
|
|
required={"rule_id", "count", "context", "reason"},
|
|
label=f"{path}.expected_suppressions[{index}]",
|
|
)
|
|
rule_id = expected["rule_id"]
|
|
count = expected["count"]
|
|
context = expected["context"]
|
|
if rule_id not in rules or not rules[rule_id].exclude_if:
|
|
raise AssertionError(f"{path}: suppression names a non-excluding rule")
|
|
if context not in screening.CONTEXTS:
|
|
raise AssertionError(f"{path}: suppression context is unknown")
|
|
if type(count) is not int or count < 1:
|
|
raise AssertionError(f"{path}: suppression count must be positive int")
|
|
if expected["reason"] != "exclude_if_within_token_window":
|
|
raise AssertionError(f"{path}: unsupported suppression reason")
|
|
key = (rule_id, context)
|
|
if key in expected_suppressions:
|
|
raise AssertionError(f"{path}: duplicate expected suppression {key!r}")
|
|
expected_suppressions[key] = count
|
|
if _actual_suppressions(text, segments, rules) != expected_suppressions:
|
|
raise AssertionError(f"{path}: suppression oracle does not replay")
|
|
|
|
non_match_reasons: set[str] = set()
|
|
seen_non_matches: set[str] = set()
|
|
for index, raw_expected in enumerate(expectation["expected_non_matches"]):
|
|
expected = _expect_exact_keys(
|
|
raw_expected,
|
|
required={"rule_id", "reason"},
|
|
label=f"{path}.expected_non_matches[{index}]",
|
|
)
|
|
rule_id = expected["rule_id"]
|
|
reason = expected["reason"]
|
|
if rule_id not in rules:
|
|
raise AssertionError(f"{path}: non-match names unknown rule {rule_id!r}")
|
|
if rule_id in seen_non_matches:
|
|
raise AssertionError(f"{path}: duplicate non-match rule {rule_id!r}")
|
|
seen_non_matches.add(rule_id)
|
|
if any(match["pattern_id"] == rule_id for match in report["matches"]):
|
|
raise AssertionError(f"{path}: expected non-match {rule_id!r} matched")
|
|
if not _non_match_reason_holds(reason, rules[rule_id], text, segments):
|
|
raise AssertionError(f"{path}: non-match reason {reason!r} did not replay")
|
|
non_match_reasons.add(reason)
|
|
return normalization_cases | {f"nonmatch:{reason}" for reason in non_match_reasons}
|
|
|
|
|
|
def _corpus_surface_text(entry: dict, surface: str) -> str | None:
|
|
return entry["title"] if surface == "title" else entry.get("abstract")
|
|
|
|
|
|
def _replay_corpus_expectation(
|
|
expectation: dict,
|
|
rules: dict[str, screening.CompiledRule],
|
|
) -> set[str]:
|
|
expectation = _expect_exact_keys(
|
|
expectation,
|
|
required={"path", "records"},
|
|
label="corpus expectation",
|
|
)
|
|
if expectation["path"] != "corpus_input.yaml":
|
|
raise AssertionError("corpus expectation path drifted")
|
|
source_entries = {
|
|
entry["citation_key"]: entry for entry in _passport()["literature_corpus"]
|
|
}
|
|
output_entries = {
|
|
entry["citation_key"]: entry for entry in _enriched()["literature_corpus"]
|
|
}
|
|
records = expectation["records"]
|
|
if not isinstance(records, list):
|
|
raise AssertionError("corpus records must be an array")
|
|
citation_keys = [
|
|
record.get("citation_key") if isinstance(record, dict) else None
|
|
for record in records
|
|
]
|
|
if (
|
|
any(not isinstance(key, str) or not key for key in citation_keys)
|
|
or len(citation_keys) != len(set(citation_keys))
|
|
or set(citation_keys) != set(source_entries)
|
|
or set(citation_keys) != set(output_entries)
|
|
):
|
|
raise AssertionError("corpus expectation citation inventory drifted")
|
|
|
|
replayed_reasons: set[str] = set()
|
|
surface_contexts = {"title": "cited_title", "abstract": "cited_abstract"}
|
|
for record_index, raw_record in enumerate(records):
|
|
record = _expect_exact_keys(
|
|
raw_record,
|
|
required={"citation_key", "surfaces"},
|
|
label=f"corpus.records[{record_index}]",
|
|
)
|
|
citation_key = record["citation_key"]
|
|
surfaces = _expect_exact_keys(
|
|
record["surfaces"],
|
|
required={"title", "abstract"},
|
|
label=f"corpus.records[{citation_key}].surfaces",
|
|
)
|
|
source_entry = source_entries[citation_key]
|
|
output_entry = output_entries[citation_key]
|
|
signals = [
|
|
signal
|
|
for signal in output_entry["bibliographic_integrity_signals"]
|
|
if signal.get("schema_version") == screening.SIGNAL_VERSION
|
|
and signal.get("signal_type") == "tortured_phrase_match"
|
|
]
|
|
if len(signals) != 2:
|
|
raise AssertionError(
|
|
f"{citation_key}: expected exactly the title and abstract signals"
|
|
)
|
|
for surface, context in surface_contexts.items():
|
|
expected = _expect_exact_keys(
|
|
surfaces[surface],
|
|
required={"check_status", "reason", "expected_rule_ids"},
|
|
optional={"non_match_reasons"},
|
|
label=f"corpus.records[{citation_key}].{surface}",
|
|
)
|
|
matching_signals = [
|
|
signal
|
|
for signal in signals
|
|
if signal["tortured_phrase_context"]["surface"] == context
|
|
]
|
|
if len(matching_signals) != 1:
|
|
raise AssertionError(
|
|
f"{citation_key}.{surface}: expected exactly one current signal"
|
|
)
|
|
signal = matching_signals[0]
|
|
signal_context = signal["tortured_phrase_context"]
|
|
if signal["check_status"] != expected["check_status"]:
|
|
raise AssertionError(f"{citation_key}.{surface}: status drifted")
|
|
if signal_context["reason_code"] != expected["reason"]:
|
|
raise AssertionError(f"{citation_key}.{surface}: reason drifted")
|
|
|
|
expected_rule_ids = expected["expected_rule_ids"]
|
|
if (
|
|
not isinstance(expected_rule_ids, list)
|
|
or any(not isinstance(rule_id, str) for rule_id in expected_rule_ids)
|
|
or len(expected_rule_ids) != len(set(expected_rule_ids))
|
|
or any(rule_id not in rules for rule_id in expected_rule_ids)
|
|
):
|
|
raise AssertionError(
|
|
f"{citation_key}.{surface}: expected_rule_ids are invalid"
|
|
)
|
|
actual_rule_ids = {
|
|
match["pattern_id"] for match in signal_context["matches"]
|
|
}
|
|
if actual_rule_ids != set(expected_rule_ids):
|
|
raise AssertionError(f"{citation_key}.{surface}: rule ids drifted")
|
|
expected_finding = (
|
|
"unresolved"
|
|
if expected["check_status"] != "checked"
|
|
else "detected" if expected_rule_ids else "not_detected"
|
|
)
|
|
if signal["finding"] != expected_finding:
|
|
raise AssertionError(f"{citation_key}.{surface}: finding drifted")
|
|
|
|
raw_non_matches = expected.get("non_match_reasons", [])
|
|
if expected["check_status"] == "checked" and not expected_rule_ids:
|
|
if not raw_non_matches:
|
|
raise AssertionError(
|
|
f"{citation_key}.{surface}: zero result lacks a rule-bound reason"
|
|
)
|
|
text = _corpus_surface_text(source_entry, surface)
|
|
seen_non_match_rules: set[str] = set()
|
|
for reason_index, raw_reason in enumerate(raw_non_matches):
|
|
reason_record = _expect_exact_keys(
|
|
raw_reason,
|
|
required={"rule_id", "reason"},
|
|
label=(
|
|
f"corpus.records[{citation_key}].{surface}."
|
|
f"non_match_reasons[{reason_index}]"
|
|
),
|
|
)
|
|
rule_id = reason_record["rule_id"]
|
|
reason = reason_record["reason"]
|
|
if rule_id not in rules or rule_id in seen_non_match_rules:
|
|
raise AssertionError(
|
|
f"{citation_key}.{surface}: invalid non-match rule {rule_id!r}"
|
|
)
|
|
seen_non_match_rules.add(rule_id)
|
|
if rule_id in actual_rule_ids or text is None:
|
|
raise AssertionError(
|
|
f"{citation_key}.{surface}: non-match rule is not absent"
|
|
)
|
|
one_segment = [screening.Segment("SEG-000001", context, 0, len(text))]
|
|
if not _non_match_reason_holds(
|
|
reason,
|
|
rules[rule_id],
|
|
text,
|
|
one_segment,
|
|
):
|
|
raise AssertionError(
|
|
f"{citation_key}.{surface}: non-match reason did not replay"
|
|
)
|
|
replayed_reasons.add(reason)
|
|
return replayed_reasons
|
|
|
|
|
|
def _replay_seed_expectations(expectations: dict) -> None:
|
|
expectations = _expect_exact_keys(
|
|
expectations,
|
|
required={
|
|
"schema_version",
|
|
"snapshot_id",
|
|
"snapshot_sha256",
|
|
"label_scope",
|
|
"empirical_accuracy_claimed",
|
|
"contextual_false_positive_labels_provided",
|
|
"contextual_false_negative_labels_provided",
|
|
"inputs",
|
|
},
|
|
label="seed expectations",
|
|
)
|
|
if expectations["schema_version"] != "tortured-phrase-seed-expectations/1.0":
|
|
raise AssertionError("seed expectations schema_version drifted")
|
|
bundle = _bundle()
|
|
if (
|
|
expectations["snapshot_id"] != bundle.snapshot["snapshot_id"]
|
|
or expectations["snapshot_id"] != bundle.manifest["snapshot_id"]
|
|
or expectations["snapshot_sha256"] != bundle.snapshot_sha256
|
|
):
|
|
raise AssertionError("seed expectations snapshot identity drifted")
|
|
if expectations["label_scope"] != "mechanical_matcher_conformance_only":
|
|
raise AssertionError("seed expectations label scope drifted")
|
|
for flag in (
|
|
"empirical_accuracy_claimed",
|
|
"contextual_false_positive_labels_provided",
|
|
"contextual_false_negative_labels_provided",
|
|
):
|
|
if expectations[flag] is not False:
|
|
raise AssertionError(f"seed expectations {flag} must remain false")
|
|
|
|
inputs = expectations["inputs"]
|
|
if not isinstance(inputs, list):
|
|
raise AssertionError("seed expectations inputs must be an array")
|
|
paths = [item.get("path") if isinstance(item, dict) else None for item in inputs]
|
|
required_paths = {"own_draft.md", "own_draft.tex", "corpus_input.yaml"}
|
|
if (
|
|
any(not isinstance(path, str) for path in paths)
|
|
or len(paths) != len(set(paths))
|
|
or set(paths) != required_paths
|
|
):
|
|
raise AssertionError("seed expectations input inventory drifted")
|
|
|
|
rules = _compiled_rules()
|
|
draft_coverage: set[str] = set()
|
|
corpus_non_match_reasons: set[str] = set()
|
|
for expectation in inputs:
|
|
if expectation["path"] == "corpus_input.yaml":
|
|
corpus_non_match_reasons |= _replay_corpus_expectation(expectation, rules)
|
|
else:
|
|
draft_coverage |= _replay_draft_expectation(expectation, rules)
|
|
required_normalization = {
|
|
"nfkc_casefold",
|
|
"same_line_dash_separator",
|
|
"soft_hyphen_join",
|
|
"line_break_hyphen_join",
|
|
}
|
|
if not required_normalization.issubset(draft_coverage):
|
|
raise AssertionError("seed expectations normalization coverage drifted")
|
|
if "nonmatch:token_boundary" not in draft_coverage:
|
|
raise AssertionError("seed expectations draft token-boundary negative is missing")
|
|
if corpus_non_match_reasons != {
|
|
"token_boundary",
|
|
"exclude_if_in_same_segment",
|
|
}:
|
|
raise AssertionError("seed expectations corpus non-match coverage drifted")
|
|
|
|
|
|
def _strict_dump(path: Path, value: dict) -> None:
|
|
path.write_text(
|
|
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _snapshot_pair(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
|
|
snapshot = json.loads((FIXTURES / "snapshot.json").read_text(encoding="utf-8"))
|
|
manifest = json.loads(
|
|
(FIXTURES / "snapshot_manifest.json").read_text(encoding="utf-8")
|
|
)
|
|
snapshot_path = tmp_path / "snapshot.json"
|
|
manifest_path = tmp_path / "manifest.json"
|
|
_strict_dump(snapshot_path, snapshot)
|
|
manifest["snapshot_sha256"] = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
|
|
_strict_dump(manifest_path, manifest)
|
|
return snapshot_path, manifest_path, snapshot, manifest
|
|
|
|
|
|
def _rewrite_pair(
|
|
snapshot_path: Path,
|
|
manifest_path: Path,
|
|
snapshot: dict,
|
|
manifest: dict,
|
|
) -> None:
|
|
_strict_dump(snapshot_path, snapshot)
|
|
manifest["snapshot_sha256"] = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
|
|
manifest["rule_count"] = len(snapshot.get("rules", []))
|
|
_strict_dump(manifest_path, manifest)
|
|
|
|
|
|
def test_snapshot_and_manifest_exact_hash_bind() -> None:
|
|
bundle = _bundle()
|
|
assert bundle.snapshot_sha256 == "962879909bfdd338047dc4569ff42188c4200fcd56a0a459b0e0f9169f2446c4"
|
|
assert len(bundle.rules) == 17
|
|
assert bundle.manifest["unsupported_rule_count"] == 0
|
|
assert bundle.manifest["rights"]["basis"] == "synthetic_fixture"
|
|
|
|
|
|
def test_seed_expectations_are_fully_replayed_as_the_mechanical_oracle() -> None:
|
|
_replay_seed_expectations(_seed_expectations())
|
|
|
|
|
|
def _expectation_input(expectations: dict, path: str) -> dict:
|
|
return next(item for item in expectations["inputs"] if item["path"] == path)
|
|
|
|
|
|
def _corpus_surface(expectations: dict, citation_key: str, surface: str) -> dict:
|
|
corpus = _expectation_input(expectations, "corpus_input.yaml")
|
|
record = next(
|
|
item for item in corpus["records"] if item["citation_key"] == citation_key
|
|
)
|
|
return record["surfaces"][surface]
|
|
|
|
|
|
def _mutate_seed_expectations(expectations: dict, mutation: str) -> None:
|
|
markdown = _expectation_input(expectations, "own_draft.md")
|
|
if mutation == "wrong_count":
|
|
markdown["expected_counts"]["rule_match_count"] += 1
|
|
elif mutation == "wrong_rule":
|
|
markdown["expected_matches"][0]["rule_id"] = "syn_corpus_abstract"
|
|
elif mutation == "wrong_context":
|
|
markdown["expected_matches"][0]["context"] = "quote"
|
|
elif mutation == "wrong_alternative":
|
|
expected = next(
|
|
item
|
|
for item in markdown["expected_matches"]
|
|
if "matched_alternative_index" in item
|
|
)
|
|
expected["matched_alternative_index"] += 1
|
|
elif mutation == "wrong_overlap":
|
|
expected = next(
|
|
item for item in markdown["expected_matches"] if "overlap_group" in item
|
|
)
|
|
expected["overlap_group"] += "-mutated"
|
|
elif mutation == "wrong_normalization":
|
|
expected = next(
|
|
item
|
|
for item in markdown["expected_matches"]
|
|
if item.get("normalization_case") == "nfkc_casefold"
|
|
)
|
|
expected["normalization_case"] = "same_line_dash_separator"
|
|
elif mutation == "wrong_suppression":
|
|
markdown["expected_suppressions"][0]["count"] += 1
|
|
elif mutation == "wrong_nonmatch":
|
|
markdown["expected_non_matches"][0]["reason"] = "exclude_if_in_same_segment"
|
|
elif mutation == "wrong_corpus_status":
|
|
_corpus_surface(expectations, "fixture_complete_2026", "title")[
|
|
"check_status"
|
|
] = "not_checked"
|
|
elif mutation == "wrong_corpus_reason":
|
|
_corpus_surface(expectations, "fixture_missing_2026", "abstract")[
|
|
"reason"
|
|
] = "ABSTRACT_EMPTY"
|
|
elif mutation == "wrong_corpus_rule_id":
|
|
_corpus_surface(expectations, "fixture_complete_2026", "title")[
|
|
"expected_rule_ids"
|
|
] = ["syn_corpus_abstract"]
|
|
elif mutation == "wrong_corpus_nonmatch":
|
|
_corpus_surface(expectations, "fixture_negative_2026", "title")[
|
|
"non_match_reasons"
|
|
][0]["rule_id"] = "syn_literal_author"
|
|
elif mutation == "wrong_corpus_nonmatch_reason":
|
|
_corpus_surface(expectations, "fixture_negative_2026", "title")[
|
|
"non_match_reasons"
|
|
][0]["reason"] = "exclude_if_in_same_segment"
|
|
elif mutation == "wrong_snapshot_identity":
|
|
expectations["snapshot_sha256"] = "0" * 64
|
|
elif mutation == "wrong_claim_flag":
|
|
expectations["empirical_accuracy_claimed"] = True
|
|
elif mutation == "unknown_root_key":
|
|
expectations["unexpected"] = True
|
|
elif mutation == "missing_root_key":
|
|
expectations.pop("schema_version")
|
|
elif mutation == "unknown_input":
|
|
expectations["inputs"].append({"path": "unknown.txt"})
|
|
elif mutation == "missing_input":
|
|
expectations["inputs"].pop()
|
|
elif mutation == "duplicate_input":
|
|
expectations["inputs"].append(copy.deepcopy(markdown))
|
|
else: # pragma: no cover - test parameter list is closed below
|
|
raise AssertionError(f"unknown test mutation {mutation!r}")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"wrong_count",
|
|
"wrong_rule",
|
|
"wrong_context",
|
|
"wrong_alternative",
|
|
"wrong_overlap",
|
|
"wrong_normalization",
|
|
"wrong_suppression",
|
|
"wrong_nonmatch",
|
|
"wrong_corpus_status",
|
|
"wrong_corpus_reason",
|
|
"wrong_corpus_rule_id",
|
|
"wrong_corpus_nonmatch",
|
|
"wrong_corpus_nonmatch_reason",
|
|
"wrong_snapshot_identity",
|
|
"wrong_claim_flag",
|
|
"unknown_root_key",
|
|
"missing_root_key",
|
|
"unknown_input",
|
|
"missing_input",
|
|
"duplicate_input",
|
|
],
|
|
)
|
|
def test_seed_expectation_mutations_fail_closed(mutation: str) -> None:
|
|
expectations = copy.deepcopy(_seed_expectations())
|
|
_mutate_seed_expectations(expectations, mutation)
|
|
with pytest.raises(AssertionError):
|
|
_replay_seed_expectations(expectations)
|
|
|
|
|
|
def test_context_detection_is_never_suppressed() -> None:
|
|
report = _report()
|
|
assert report["check_status"] == "checked"
|
|
assert report["finding"] == "detected"
|
|
assert report["counts"]["unknown_segments"] == 0
|
|
by_rule: dict[str, list[dict]] = {}
|
|
for match in report["matches"]:
|
|
by_rule.setdefault(match["pattern_id"], []).append(match)
|
|
assert by_rule["syn_context_quote"][0]["context"] == "quote"
|
|
assert {item["context"] for item in by_rule["syn_context_code"]} == {
|
|
"code_or_verbatim"
|
|
}
|
|
assert by_rule["syn_context_reference"][0]["context"] == "reference_entry"
|
|
assert all(
|
|
item["disposition"] == "preserve_verbatim_review_context"
|
|
for rule_id in ("syn_context_quote", "syn_context_code", "syn_context_reference")
|
|
for item in by_rule[rule_id]
|
|
)
|
|
|
|
|
|
def test_exclude_if_is_segment_scoped() -> None:
|
|
matches = [
|
|
item for item in _report()["matches"] if item["pattern_id"] == "syn_exclude_segment"
|
|
]
|
|
assert len(matches) == 1
|
|
assert matches[0]["context"] == "quote"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("text", "artifact_format"),
|
|
[
|
|
("moonlit pickle\n\nrecipe glossary", "markdown"),
|
|
(
|
|
"# References\nmoonlit pickle\nrecipe glossary\n",
|
|
"markdown",
|
|
),
|
|
(
|
|
"\\begin{thebibliography}{9}\n"
|
|
"\\bibitem{a} moonlit pickle\n"
|
|
"\\bibitem{b} recipe glossary\n"
|
|
"\\end{thebibliography}\n",
|
|
"latex",
|
|
),
|
|
],
|
|
)
|
|
def test_exclude_if_never_crosses_paragraph_or_reference_entry(
|
|
text: str, artifact_format: str
|
|
) -> None:
|
|
rule = _compiled_rules()["syn_exclude_segment"]
|
|
segments = screening.segment_document(text, artifact_format)
|
|
retained = [
|
|
witness
|
|
for segment in segments
|
|
for witness in screening.evaluate_rule(
|
|
rule, screening.tokenize(text[segment.start : segment.end])
|
|
)
|
|
]
|
|
assert len(retained) == 1
|
|
|
|
|
|
def test_overlaps_preserve_rule_hits_and_cluster_unique_instances() -> None:
|
|
matches = [
|
|
item
|
|
for item in _report()["matches"]
|
|
if item["pattern_id"] in {"syn_overlap_long", "syn_overlap_short"}
|
|
]
|
|
assert len(matches) == 2
|
|
assert screening._unique_instance_count(matches) == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"rule_id",
|
|
["syn_nfkc_casefold", "syn_dash_same_line", "syn_soft_hyphen", "syn_hyphen_line_break"],
|
|
)
|
|
def test_frozen_normalization_cases_match(rule_id: str) -> None:
|
|
assert any(item["pattern_id"] == rule_id for item in _report()["matches"])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw", "expected", "expected_start"),
|
|
[
|
|
("a-\nb", ["ab"], 0),
|
|
("silver- \nleaf", ["silver", "leaf"], 0),
|
|
("silver—\nleaf", ["silver", "leaf"], 0),
|
|
("1-\na", ["1", "a"], 0),
|
|
("a-\n2", ["a", "2"], 0),
|
|
("a1-\nb", ["a1", "b"], 0),
|
|
("a\u0301-\nb", ["á", "b"], 0),
|
|
("\u0301alpha", ["alpha"], 1),
|
|
],
|
|
)
|
|
def test_only_exact_ascii_immediate_line_hyphen_joins_and_marks_cannot_start(
|
|
raw: str, expected: list[str], expected_start: int
|
|
) -> None:
|
|
tokens = screening.tokenize(raw)
|
|
assert [token.normalized for token in tokens] == expected
|
|
assert tokens[0].start == expected_start
|
|
|
|
|
|
def test_token_boundary_prevents_substring_match() -> None:
|
|
assert all(item["pattern_id"] != "syn_boundary_orb" for item in _report()["matches"])
|
|
|
|
|
|
def test_repeated_literal_hits_fail_closed_at_the_witness_cap() -> None:
|
|
tokens = screening.tokenize(" ".join(["orb"] * (screening.MAX_NODE_WITNESSES + 1)))
|
|
with pytest.raises(screening.MatchLimitError, match="literal produced more"):
|
|
screening.evaluate_expression({"op": "literal", "value": "orb"}, tokens)
|
|
|
|
|
|
def test_witness_reducer_deduplicates_only_exact_spans() -> None:
|
|
inner = screening.Witness(2, 3, 2, 3)
|
|
same = screening.Witness(2, 3, 2, 3)
|
|
outer = screening.Witness(1, 4, 1, 4)
|
|
separate = screening.Witness(5, 6, 5, 6)
|
|
assert screening._minimal_witnesses([outer, inner, same, separate]) == [
|
|
outer,
|
|
inner,
|
|
separate,
|
|
]
|
|
|
|
|
|
def test_any_and_all_preserve_distinct_nested_witnesses() -> None:
|
|
tokens = screening.tokenize("amber reed reed")
|
|
any_values = screening.evaluate_expression(
|
|
{
|
|
"op": "any",
|
|
"alternatives": [
|
|
{"op": "literal", "value": "amber"},
|
|
{"op": "literal", "value": "amber reed"},
|
|
],
|
|
},
|
|
tokens,
|
|
)
|
|
assert [(item.token_start, item.token_end) for item in any_values] == [
|
|
(0, 1),
|
|
(0, 2),
|
|
]
|
|
all_values = screening.evaluate_expression(
|
|
{
|
|
"op": "all",
|
|
"terms": [
|
|
{"op": "literal", "value": "amber"},
|
|
{"op": "literal", "value": "reed"},
|
|
],
|
|
"max_span_tokens": 3,
|
|
},
|
|
tokens,
|
|
)
|
|
assert [(item.token_start, item.token_end) for item in all_values] == [
|
|
(0, 2),
|
|
(0, 3),
|
|
]
|
|
|
|
|
|
def test_full_scan_counts_nested_same_rule_witnesses_as_one_overlap_instance() -> None:
|
|
expression = {
|
|
"op": "any",
|
|
"alternatives": [
|
|
{"op": "literal", "value": "amber"},
|
|
{"op": "literal", "value": "amber reed"},
|
|
],
|
|
}
|
|
base = _bundle()
|
|
rule = screening.CompiledRule(
|
|
rule_id="nested_union",
|
|
expression=expression,
|
|
exclude_if=(),
|
|
rule_sha256=screening._sha256_text(screening._canonical_json(expression)),
|
|
semantic_key=screening._canonical_json(expression),
|
|
)
|
|
bundle = screening.SnapshotBundle(
|
|
snapshot=base.snapshot,
|
|
manifest=base.manifest,
|
|
snapshot_sha256=base.snapshot_sha256,
|
|
manifest_sha256=base.manifest_sha256,
|
|
rules=(rule,),
|
|
unicode_data_version=base.unicode_data_version,
|
|
)
|
|
text = "amber reed"
|
|
matches, counts = screening.scan_segments(
|
|
text,
|
|
[screening.Segment("SEG-000001", "author_prose", 0, len(text))],
|
|
bundle,
|
|
artifact_sha256=hashlib.sha256(text.encode()).hexdigest(),
|
|
surface="own_draft",
|
|
)
|
|
assert len(matches) == 2
|
|
assert counts["rule_match_count"] == 2
|
|
assert counts["matched_rule_count"] == 1
|
|
assert counts["unique_instance_count"] == 1
|
|
|
|
|
|
def test_utf8_and_codepoint_spans_replay_multibyte_text() -> None:
|
|
report = screening.build_own_draft_report(
|
|
"前文 luminous turnip 後文",
|
|
artifact_id="unicode.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
match = next(item for item in report["matches"] if item["pattern_id"] == "syn_literal_author")
|
|
span = match["source_span"]
|
|
text = "前文 luminous turnip 後文"
|
|
assert text[span["codepoint_start"] : span["codepoint_end"]] == match["matched_text"]
|
|
raw = text.encode("utf-8")
|
|
assert raw[span["utf8_start"] : span["utf8_end"]].decode() == match["matched_text"]
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["own_draft.md", "own_draft.tex"])
|
|
def test_segment_partition_covers_source_exactly(name: str) -> None:
|
|
text = _draft(name)
|
|
segments = screening.segment_document(text, "latex" if name.endswith(".tex") else "markdown")
|
|
assert segments[0].start == 0
|
|
assert segments[-1].end == len(text)
|
|
assert all(left.end == right.start for left, right in zip(segments, segments[1:]))
|
|
|
|
|
|
def test_reference_heading_inside_fence_does_not_reclassify_following_prose() -> None:
|
|
text = "```text\n# References\n```\nFollowing luminous turnip."
|
|
segments = screening.segment_document(text, "markdown")
|
|
following = next(item for item in segments if item.start <= text.index("Following") < item.end)
|
|
assert following.kind == "author_prose"
|
|
|
|
|
|
def test_reference_heading_inside_html_comment_does_not_reclassify_prose() -> None:
|
|
text = "<!--\n# References\n-->\nordinary luminous turnip\n"
|
|
segments = screening.segment_document(text, "markdown")
|
|
following = next(
|
|
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
)
|
|
assert following.kind == "author_prose"
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="comment-heading.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
prose_match = next(
|
|
item for item in report["matches"] if item["matched_text"] == "luminous turnip"
|
|
)
|
|
assert prose_match["context"] == "author_prose"
|
|
|
|
|
|
@pytest.mark.parametrize("marker", ["```python", "~~~text"])
|
|
def test_fence_opener_inside_html_comment_cannot_leak_unknown_context(
|
|
marker: str,
|
|
) -> None:
|
|
text = f"<!--\n{marker}\n-->\nordinary luminous turnip\n"
|
|
segments = screening.segment_document(text, "markdown")
|
|
following = next(
|
|
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
)
|
|
assert following.kind == "author_prose"
|
|
assert all(item.kind != "unknown" for item in segments)
|
|
|
|
|
|
def test_latex_environment_opener_inside_comment_cannot_leak_context() -> None:
|
|
text = (
|
|
"% \\begin{thebibliography}{9}\n"
|
|
"ordinary luminous turnip\n"
|
|
"\\end{thebibliography}\n"
|
|
)
|
|
segments = screening.segment_document(text, "latex")
|
|
following = next(
|
|
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
)
|
|
assert following.kind == "author_prose"
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="comment-environment.tex",
|
|
artifact_format="latex",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
prose_match = next(
|
|
item for item in report["matches"] if item["matched_text"] == "luminous turnip"
|
|
)
|
|
assert prose_match["context"] == "author_prose"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("environment", "expected_kind"),
|
|
[
|
|
("thebibliography", "reference_entry"),
|
|
("quote", "quote"),
|
|
("verbatim", "code_or_verbatim"),
|
|
],
|
|
)
|
|
def test_latex_environment_escape_parity(
|
|
environment: str, expected_kind: str
|
|
) -> None:
|
|
even = (
|
|
"\\" * 2
|
|
+ f"begin{{{environment}}}\nordinary\n"
|
|
+ "\\" * 2
|
|
+ f"end{{{environment}}}\n"
|
|
)
|
|
assert all(
|
|
item.kind == "author_prose"
|
|
for item in screening.segment_document(even, "latex")
|
|
)
|
|
odd = (
|
|
"\\" * 3
|
|
+ f"begin{{{environment}}}\nordinary\n"
|
|
+ "\\" * 3
|
|
+ f"end{{{environment}}}\n"
|
|
)
|
|
assert any(
|
|
item.kind == expected_kind
|
|
for item in screening.segment_document(odd, "latex")
|
|
)
|
|
|
|
|
|
def test_latex_verb_escape_parity_and_opaque_opener_precedence() -> None:
|
|
even = r"\\verb|ordinary luminous turnip|"
|
|
assert all(
|
|
item.kind == "author_prose"
|
|
for item in screening.segment_document(even, "latex")
|
|
)
|
|
active = r"\verb|\begin{verbatim}| ordinary luminous turnip"
|
|
segments = screening.segment_document(active, "latex")
|
|
following = next(
|
|
item for item in segments if item.start <= active.index("ordinary") < item.end
|
|
)
|
|
assert following.kind == "author_prose"
|
|
assert all(item.kind != "unknown" for item in segments)
|
|
|
|
|
|
def test_source_order_opaque_lexer_does_not_let_inner_opener_consume_later_close() -> None:
|
|
text = (
|
|
r"\verb|\begin{verbatim}| "
|
|
r"\begin{verbatim} recipe glossary \end{verbatim} moonlit pickle"
|
|
)
|
|
segments = screening.segment_document(text, "latex")
|
|
real_environment = text.index(r"\begin{verbatim}", len(r"\verb|"))
|
|
later_prose = text.index("moonlit pickle")
|
|
assert next(
|
|
item for item in segments if item.start <= real_environment < item.end
|
|
).kind == "code_or_verbatim"
|
|
assert next(
|
|
item for item in segments if item.start <= later_prose < item.end
|
|
).kind == "author_prose"
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="source-order.tex",
|
|
artifact_format="latex",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert any(
|
|
item["pattern_id"] == "syn_exclude_segment"
|
|
and item["matched_text"] == "moonlit pickle"
|
|
for item in report["matches"]
|
|
)
|
|
|
|
|
|
def test_source_order_markdown_comment_does_not_consume_later_inline_code() -> None:
|
|
text = "<!-- ` --> `luminous turnip`"
|
|
segments = screening.segment_document(text, "markdown")
|
|
later = text.index("`luminous")
|
|
assert next(item for item in segments if item.start <= later < item.end).kind == (
|
|
"code_or_verbatim"
|
|
)
|
|
assert all(item.kind != "unknown" for item in segments)
|
|
|
|
|
|
def test_blockquote_marker_inside_comment_cannot_capture_later_prose() -> None:
|
|
text = '<!--\n> --> "recipe glossary" moonlit pickle\n'
|
|
segments = screening.segment_document(text, "markdown")
|
|
target = text.index("moonlit pickle")
|
|
assert next(item for item in segments if item.start <= target < item.end).kind == (
|
|
"author_prose"
|
|
)
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="comment-blockquote.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert any(
|
|
item["pattern_id"] == "syn_exclude_segment"
|
|
and item["matched_text"] == "moonlit pickle"
|
|
for item in report["matches"]
|
|
)
|
|
|
|
|
|
def test_context_openers_inside_opaque_bytes_do_not_consume_later_context() -> None:
|
|
text = '`"` "recipe glossary" moonlit pickle '
|
|
text += '`[` [Visible title](https://doi.org/10.1/example)'
|
|
segments = screening.segment_document(text, "markdown")
|
|
glossary = text.index('"recipe glossary"')
|
|
title = text.index("Visible title")
|
|
assert next(item for item in segments if item.start <= glossary < item.end).kind == (
|
|
"quote"
|
|
)
|
|
assert next(item for item in segments if item.start <= title < item.end).kind == (
|
|
"cited_title"
|
|
)
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="opaque-context.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert any(
|
|
item["pattern_id"] == "syn_exclude_segment"
|
|
and item["matched_text"] == "moonlit pickle"
|
|
for item in report["matches"]
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text",
|
|
[
|
|
r"\verbose ordinary luminous turnip",
|
|
r"\verbatim ordinary luminous turnip",
|
|
],
|
|
)
|
|
def test_latex_verb_control_word_prefixes_remain_prose(text: str) -> None:
|
|
assert all(
|
|
item.kind == "author_prose"
|
|
for item in screening.segment_document(text, "latex")
|
|
)
|
|
|
|
|
|
def test_latex_starred_verb_and_nested_quote_are_recognized() -> None:
|
|
starred = r"\verb*|ordinary luminous turnip|"
|
|
assert any(
|
|
item.kind == "code_or_verbatim"
|
|
and starred[item.start : item.end] == starred
|
|
for item in screening.segment_document(starred, "latex")
|
|
)
|
|
nested = (
|
|
r"\begin{quote} outer \begin{quote} inner \end{quote} "
|
|
r"tail \end{quote}"
|
|
)
|
|
tail = nested.index("tail")
|
|
assert next(
|
|
item
|
|
for item in screening.segment_document(nested, "latex")
|
|
if item.start <= tail < item.end
|
|
).kind == "quote"
|
|
|
|
|
|
@pytest.mark.parametrize("text", [r"\verb ", "\\verb\n", r"\verb* "])
|
|
def test_malformed_bare_latex_verb_is_unknown(text: str) -> None:
|
|
assert any(
|
|
item.kind == "unknown"
|
|
for item in screening.segment_document(text, "latex")
|
|
)
|
|
|
|
|
|
def test_starred_latex_verb_cannot_backtrack_into_unstarred_delimiter() -> None:
|
|
text = r"\verb* foo * ordinary luminous turnip"
|
|
segments = screening.segment_document(text, "latex")
|
|
assert len(segments) == 1
|
|
assert segments[0].kind == "unknown"
|
|
|
|
|
|
def test_latex_accent_macro_quotes_remain_author_prose() -> None:
|
|
text = r'\"o ordinary luminous turnip \"u'
|
|
assert all(
|
|
item.kind == "author_prose"
|
|
for item in screening.segment_document(text, "latex")
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target",
|
|
[
|
|
"HTTPS://DOI.ORG/10.1/example",
|
|
"HTTP://DX.DOI.ORG/10.1/example",
|
|
"DOI:10.1/example",
|
|
],
|
|
)
|
|
def test_doi_title_prefix_is_ascii_case_insensitive(target: str) -> None:
|
|
text = f"[Cited Turnip]({target})"
|
|
title = text.index("Cited Turnip")
|
|
assert next(
|
|
item
|
|
for item in screening.segment_document(text, "markdown")
|
|
if item.start <= title < item.end
|
|
).kind == "cited_title"
|
|
|
|
|
|
def test_opaque_opener_cap_precedes_unclosed_verb_pairing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_PARSE_INTERVALS", 3)
|
|
with pytest.raises(screening.MatchLimitError, match="opaque parse candidates"):
|
|
screening.segment_document(r"\verb!" * 4, "latex")
|
|
|
|
|
|
def test_unclosed_context_scanners_advance_monotonically() -> None:
|
|
text = "[" * 20_000 + "\n" + "“" * 10_000
|
|
segments = screening.segment_document(text, "markdown")
|
|
assert len(segments) == 1
|
|
assert segments[0].kind == "author_prose"
|
|
|
|
|
|
def test_escaped_latex_environment_closer_does_not_close_early() -> None:
|
|
text = (
|
|
"\\begin{thebibliography}{9}\n"
|
|
"\\\\end{thebibliography}\n"
|
|
"ordinary luminous turnip\n"
|
|
"\\end{thebibliography}\n"
|
|
)
|
|
following = next(
|
|
item
|
|
for item in screening.segment_document(text, "latex")
|
|
if item.start <= text.index("ordinary") < item.end
|
|
)
|
|
assert following.kind == "reference_entry"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text",
|
|
[
|
|
r"\(\begin{quote}\) ordinary luminous turnip",
|
|
r"$\begin{thebibliography}$ ordinary luminous turnip",
|
|
r"\[\begin{verbatim}\] ordinary luminous turnip",
|
|
],
|
|
)
|
|
def test_latex_environment_openers_inside_math_are_opaque(text: str) -> None:
|
|
segments = screening.segment_document(text, "latex")
|
|
following = next(
|
|
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
)
|
|
assert following.kind == "author_prose"
|
|
assert all(item.kind != "unknown" for item in segments)
|
|
|
|
|
|
@pytest.mark.parametrize("artifact_format", ["markdown", "latex"])
|
|
def test_odd_backslash_escaped_dollar_is_literal_prose(
|
|
artifact_format: str,
|
|
) -> None:
|
|
text = r"Cost is \$5."
|
|
segments = screening.segment_document(text, artifact_format)
|
|
assert all(segment.kind == "author_prose" for segment in segments)
|
|
|
|
|
|
def test_markdown_backtick_escape_parity_is_respected() -> None:
|
|
escaped = r"Literal \` tick."
|
|
assert all(
|
|
segment.kind == "author_prose"
|
|
for segment in screening.segment_document(escaped, "markdown")
|
|
)
|
|
active = r"Literal \\`code` tick."
|
|
assert any(
|
|
segment.kind == "code_or_verbatim"
|
|
for segment in screening.segment_document(active, "markdown")
|
|
)
|
|
|
|
|
|
def test_latex_percent_escape_parity_is_respected() -> None:
|
|
escaped = r"Literal \% text."
|
|
assert all(
|
|
segment.kind == "author_prose"
|
|
for segment in screening.segment_document(escaped, "latex")
|
|
)
|
|
active = r"Literal \\% comment"
|
|
assert any(
|
|
segment.kind == "code_or_verbatim"
|
|
for segment in screening.segment_document(active, "latex")
|
|
)
|
|
|
|
|
|
def test_unclosed_construct_is_unknown_and_degrades_not_clean() -> None:
|
|
report = screening.build_own_draft_report(
|
|
"Ordinary prose `luminous turnip",
|
|
artifact_id="broken.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert report["check_status"] == "degraded"
|
|
assert report["finding"] == "unresolved"
|
|
assert report["reason_code"] == "DOCUMENT_PARSE_DEGRADED"
|
|
assert report["counts"]["unknown_segments"] > 0
|
|
assert report["matches"]
|
|
assert report["matches"][0]["disposition"] == "review_unknown_no_automatic_rewrite"
|
|
|
|
|
|
def test_segment_resource_cap_degrades_without_partial_matches(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_SEGMENTS", 1)
|
|
report = screening.build_own_draft_report(
|
|
"prose `code` prose",
|
|
artifact_id="many-segments.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert report["check_status"] == "degraded"
|
|
assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
|
|
assert report["matches"] == []
|
|
assert report["counts"]["rules_evaluated"] == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text",
|
|
[
|
|
"a\n\nb\n\nc\n\n",
|
|
"# References\none\ntwo\nthree\n",
|
|
],
|
|
)
|
|
def test_partition_boundaries_fail_while_collecting(
|
|
monkeypatch: pytest.MonkeyPatch, text: str
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_SEGMENTS", 2)
|
|
with pytest.raises(screening.MatchLimitError, match="document partition exceeds 2"):
|
|
screening.segment_document(text, "markdown")
|
|
|
|
|
|
def test_over_token_input_fails_before_utf8_endpoint_projection(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
bundle = _bundle()
|
|
monkeypatch.setattr(screening, "MAX_TOKENS", 1)
|
|
|
|
def unexpected_offsets(_text, _indices):
|
|
raise AssertionError("UTF-8 endpoint projection must follow token admission")
|
|
|
|
monkeypatch.setattr(screening, "_selected_byte_offsets", unexpected_offsets)
|
|
with pytest.raises(screening.MatchLimitError, match="token count exceeds 1"):
|
|
screening.scan_segments(
|
|
"alpha beta",
|
|
[screening.Segment("SEG-000001", "author_prose", 0, 10)],
|
|
bundle,
|
|
artifact_sha256=hashlib.sha256(b"alpha beta").hexdigest(),
|
|
surface="own_draft",
|
|
)
|
|
|
|
|
|
def test_raw_token_expansion_is_bounded_before_normalization(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_RAW_TOKEN_CODEPOINTS", 2)
|
|
|
|
def unexpected_normalization(_value: str):
|
|
raise AssertionError("overlong raw token must fail before NFKC expansion")
|
|
|
|
monkeypatch.setattr(screening, "_normalized_token_parts", unexpected_normalization)
|
|
with pytest.raises(screening.MatchLimitError, match="raw token exceeds 2"):
|
|
screening.tokenize("\ufdfa" * 3)
|
|
|
|
|
|
def test_raw_token_cap_accepts_n_and_rejects_n_plus_one(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_RAW_TOKEN_CODEPOINTS", 2)
|
|
assert [item.normalized for item in screening.tokenize("ab")] == ["ab"]
|
|
with pytest.raises(screening.MatchLimitError, match="raw token exceeds 2"):
|
|
screening.tokenize("abc")
|
|
|
|
|
|
def test_parser_work_budget_accepts_n_and_rejects_n_plus_one(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 2)
|
|
budget = screening.ParseWorkBudget()
|
|
budget.spend()
|
|
budget.spend()
|
|
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
|
|
budget.spend()
|
|
|
|
|
|
def test_escaped_and_doi_closer_candidates_spend_parser_budget(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 2)
|
|
accepted = screening.ParseWorkBudget()
|
|
assert screening._find_unescaped(r"\$$", "$", 0, budget=accepted) == 2
|
|
exhausted = screening.ParseWorkBudget()
|
|
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
|
|
screening._find_unescaped(r"\$\$$", "$", 0, budget=exhausted)
|
|
doi_budget = screening.ParseWorkBudget()
|
|
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
|
|
screening._doi_title_intervals(
|
|
"[title](doi:10.1/example)",
|
|
excluded=(),
|
|
budget=doi_budget,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("text", "artifact_format"),
|
|
[
|
|
("<!--\n> one\n> two\n> three\n-->", "markdown"),
|
|
(r"\end{quote}\end{quote}\end{quote}\end{quote}", "latex"),
|
|
],
|
|
)
|
|
def test_excluded_and_unmatched_context_candidates_spend_parser_budget(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
text: str,
|
|
artifact_format: str,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 3)
|
|
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 3"):
|
|
screening.segment_document(text, artifact_format)
|
|
|
|
|
|
def test_selected_utf8_offsets_are_exact_and_endpoint_bounded() -> None:
|
|
text = "A台灣B"
|
|
assert screening._selected_byte_offsets(text, {0, 1, 3, 4}) == {
|
|
0: 0,
|
|
1: 1,
|
|
3: 7,
|
|
4: 8,
|
|
}
|
|
|
|
|
|
def test_delimiter_parser_enforces_interval_cap_before_return(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_PARSE_INTERVALS", 2)
|
|
with pytest.raises(screening.MatchLimitError, match="parse interval count exceeds 2"):
|
|
screening._paired_delimiter_intervals(
|
|
"$one$ $two$ $three$", "$", "$", "code_or_verbatim"
|
|
)
|
|
|
|
|
|
def test_rule_by_segment_work_is_globally_bounded(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_RULE_SEGMENT_EVALUATIONS", 1)
|
|
with pytest.raises(
|
|
screening.MatchLimitError, match="rule-by-segment evaluations exceed 1"
|
|
):
|
|
screening.scan_segments(
|
|
"luminous turnip",
|
|
[screening.Segment("SEG-000001", "author_prose", 0, 16)],
|
|
_bundle(),
|
|
artifact_sha256=hashlib.sha256(b"luminous turnip").hexdigest(),
|
|
surface="own_draft",
|
|
)
|
|
|
|
|
|
def test_literal_and_composition_work_budget_is_shared_across_corpus(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_MATCH_WORK_UNITS", 300)
|
|
output = _enriched()
|
|
first, second = output["literature_corpus"][:2]
|
|
assert all(
|
|
signal["check_status"] == "checked"
|
|
for signal in first["bibliographic_integrity_signals"]
|
|
)
|
|
second_title = next(
|
|
signal
|
|
for signal in second["bibliographic_integrity_signals"]
|
|
if signal["tortured_phrase_context"]["surface"] == "cited_title"
|
|
)
|
|
assert second_title["check_status"] == "degraded"
|
|
assert (
|
|
second_title["tortured_phrase_context"]["reason_code"]
|
|
== "MATCH_RESOURCE_LIMIT"
|
|
)
|
|
assert second_title["tortured_phrase_context"]["matches"] == []
|
|
|
|
|
|
def test_report_digest_and_full_replay() -> None:
|
|
report = _report()
|
|
screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
|
|
report["counts"]["rule_match_count"] += 1
|
|
with pytest.raises(screening.ScreeningError, match="report_sha256"):
|
|
screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
|
|
|
|
|
|
def test_rehashed_report_still_fails_source_replay() -> None:
|
|
report = _report()
|
|
report["matches"][0]["matched_text"] = "forged"
|
|
report["matches"][0]["matched_text_sha256"] = screening._sha256_text("forged")
|
|
report["report_sha256"] = screening._report_digest(report)
|
|
with pytest.raises(screening.ScreeningError, match="replay"):
|
|
screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
|
|
|
|
|
|
def test_oversized_full_report_collapses_to_a_self_readable_degraded_artifact(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
text = " ".join(["luminous turnip"] * 100)
|
|
monkeypatch.setattr(screening, "MAX_ADVISORY_BYTES", 5000)
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="bounded.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert report["check_status"] == "degraded"
|
|
assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
|
|
assert report["matches"] == []
|
|
assert len(screening._pretty_json_bytes(report)) <= screening.MAX_ADVISORY_BYTES
|
|
path = tmp_path / "report.json"
|
|
screening._atomic_write_json(
|
|
path, report, maximum=screening.MAX_ADVISORY_BYTES
|
|
)
|
|
assert screening._load_report(path) == report
|
|
screening.validate_own_draft_report(report, text, state=_state())
|
|
|
|
|
|
def test_bounded_reader_rejects_before_consuming_beyond_limit(tmp_path: Path) -> None:
|
|
path = tmp_path / "oversized.bin"
|
|
path.write_bytes(b"x" * 9)
|
|
with pytest.raises(screening.MatchLimitError, match="exceeds 8 bytes"):
|
|
screening._read_bounded_bytes(path, maximum=8)
|
|
|
|
|
|
def test_no_snapshot_is_explicit_not_checked() -> None:
|
|
state = screening.snapshot_state(None, None)
|
|
report = screening.build_own_draft_report(
|
|
"luminous turnip",
|
|
artifact_id="draft.md",
|
|
artifact_format="markdown",
|
|
state=state,
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert report["check_status"] == "not_checked"
|
|
assert report["finding"] == "unresolved"
|
|
assert report["reason_code"] == "SNAPSHOT_NOT_PROVIDED"
|
|
assert report["matches"] == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"state",
|
|
[
|
|
screening.snapshot_state(None, None),
|
|
screening.snapshot_state(FIXTURES / "snapshot.json", None),
|
|
],
|
|
)
|
|
def test_empty_draft_preserves_missing_or_degraded_snapshot_authority(
|
|
state: screening.SnapshotState,
|
|
) -> None:
|
|
report = screening.build_own_draft_report(
|
|
"",
|
|
artifact_id="empty.md",
|
|
artifact_format="markdown",
|
|
state=state,
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert report["check_status"] == (
|
|
"not_checked" if state.status == "not_checked" else "degraded"
|
|
)
|
|
assert report["reason_code"] == state.reason_code
|
|
assert report["finding"] == "unresolved"
|
|
screening.validate_own_draft_report(report, "", state=state)
|
|
|
|
|
|
def test_empty_draft_is_degraded_never_a_zero_match() -> None:
|
|
report = screening.build_own_draft_report(
|
|
" \t\n",
|
|
artifact_id="empty.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert report["check_status"] == "degraded"
|
|
assert report["finding"] == "unresolved"
|
|
assert report["reason_code"] == "DOCUMENT_EMPTY"
|
|
assert report["matches"] == []
|
|
assert report["counts"]["rules_evaluated"] == 0
|
|
|
|
|
|
def test_snapshot_byte_tamper_fails_hash_before_content_access() -> None:
|
|
manifest = FIXTURES / "snapshot_manifest.json"
|
|
state = screening.snapshot_state(FIXTURES / "own_draft.md", manifest)
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw", "reason"),
|
|
[
|
|
(b"\xef\xbb\xbf{}", "SNAPSHOT_BYTES_INVALID"),
|
|
(b"\xff", "SNAPSHOT_BYTES_INVALID"),
|
|
(b'{"schema_version":"a","schema_version":"b"}', "SNAPSHOT_BYTES_INVALID"),
|
|
(b'{"x":NaN}', "SNAPSHOT_BYTES_INVALID"),
|
|
],
|
|
)
|
|
def test_strict_snapshot_loader_rejects_unsafe_bytes(
|
|
tmp_path: Path, raw: bytes, reason: str
|
|
) -> None:
|
|
snapshot_path = tmp_path / "bad.json"
|
|
snapshot_path.write_bytes(raw)
|
|
manifest = json.loads(
|
|
(FIXTURES / "snapshot_manifest.json").read_text(encoding="utf-8")
|
|
)
|
|
manifest["snapshot_sha256"] = hashlib.sha256(raw).hexdigest()
|
|
manifest_path = tmp_path / "manifest.json"
|
|
_strict_dump(manifest_path, manifest)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == reason
|
|
|
|
|
|
def test_draft_reader_rejects_isolated_carriage_return(tmp_path: Path) -> None:
|
|
draft = tmp_path / "draft.md"
|
|
draft.write_bytes(b"alpha\rbeta")
|
|
with pytest.raises(screening.ScreeningError, match="isolated carriage return"):
|
|
screening._read_strict_text(
|
|
draft, maximum=screening.MAX_DOCUMENT_BYTES, label="draft"
|
|
)
|
|
|
|
|
|
def test_direct_builder_allows_crlf_but_rejects_isolated_carriage_return() -> None:
|
|
screening.build_own_draft_report(
|
|
"alpha\r\nbeta",
|
|
artifact_id="draft.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
with pytest.raises(screening.ScreeningError, match="isolated carriage return"):
|
|
screening.build_own_draft_report(
|
|
"alpha\rbeta",
|
|
artifact_id="draft.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def test_manifest_hash_mismatch_never_checks_remaining_rules(tmp_path: Path) -> None:
|
|
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
snapshot["rules"][0]["expression"]["value"] = "changed phrase"
|
|
_strict_dump(snapshot_path, snapshot)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
|
|
|
|
|
|
def test_manifest_snapshot_identity_must_match_hash_bound_snapshot(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
snapshot_path, manifest_path, _snapshot, manifest = _snapshot_pair(tmp_path)
|
|
manifest["snapshot_id"] = "forged-snapshot-id"
|
|
_strict_dump(manifest_path, manifest)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"synthetic_reference",
|
|
"user_without_declaration",
|
|
"permission_without_reference",
|
|
"unresolved_but_permitted",
|
|
"null_source_locator",
|
|
],
|
|
)
|
|
def test_manifest_rights_and_source_projection_fail_closed(
|
|
tmp_path: Path, mutation: str
|
|
) -> None:
|
|
snapshot_path, manifest_path, _snapshot, manifest = _snapshot_pair(tmp_path)
|
|
if mutation == "synthetic_reference":
|
|
manifest["rights"]["reference"] = "unexpected reference"
|
|
elif mutation == "user_without_declaration":
|
|
manifest["supply_mode"] = "user_supplied"
|
|
manifest["rights"] = {
|
|
"basis": "user_declared_authorized",
|
|
"redistribution_status": "permitted",
|
|
"reference": None,
|
|
"user_declaration": None,
|
|
}
|
|
elif mutation == "permission_without_reference":
|
|
manifest["supply_mode"] = "user_supplied"
|
|
manifest["rights"] = {
|
|
"basis": "written_permission",
|
|
"redistribution_status": "permitted",
|
|
"reference": None,
|
|
"user_declaration": None,
|
|
}
|
|
elif mutation == "unresolved_but_permitted":
|
|
manifest["supply_mode"] = "user_supplied"
|
|
manifest["rights"] = {
|
|
"basis": "unresolved",
|
|
"redistribution_status": "permitted",
|
|
"reference": None,
|
|
"user_declaration": None,
|
|
}
|
|
else:
|
|
manifest["source"]["locator"] = None
|
|
_strict_dump(manifest_path, manifest)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == "SNAPSHOT_MANIFEST_INVALID"
|
|
|
|
|
|
def test_advisory_schema_mirrors_manifest_rights_conditionals() -> None:
|
|
report = _report()
|
|
validator = Draft202012Validator(
|
|
screening._load_schema(screening.ADVISORY_SCHEMA_PATH),
|
|
format_checker=Draft202012Validator.FORMAT_CHECKER,
|
|
)
|
|
rights = report["input_binding"]["snapshot"]["rights"]
|
|
rights["basis"] = "user_declared_authorized"
|
|
rights["user_declaration"] = None
|
|
assert list(validator.iter_errors(report))
|
|
|
|
|
|
def test_explicit_but_unreadable_snapshot_is_degraded_not_absent(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
missing = tmp_path / "missing-snapshot.json"
|
|
state = screening.snapshot_state(missing, FIXTURES / "snapshot_manifest.json")
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == "SNAPSHOT_BYTES_INVALID"
|
|
signal = screening.build_cited_signal(
|
|
_passport()["literature_corpus"][0],
|
|
surface="cited_title",
|
|
state=state,
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert signal["check_status"] == "degraded"
|
|
assert signal["tortured_phrase_context"]["reason_code"] == "SNAPSHOT_BYTES_INVALID"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mutation", "expected_reason"),
|
|
[
|
|
(lambda snap, man: snap.update({"grammar_profile": "unknown/9"}), "SNAPSHOT_PROFILE_UNSUPPORTED"),
|
|
(lambda snap, man: snap.update({"rules": []}), "SNAPSHOT_MANIFEST_INVALID"),
|
|
(
|
|
lambda snap, man: snap["rules"][0].update(
|
|
{"expression": {"op": "regex", "value": ".*"}}
|
|
),
|
|
"SNAPSHOT_RULES_UNSUPPORTED",
|
|
),
|
|
(
|
|
lambda snap, man: snap["rules"].append(copy.deepcopy(snap["rules"][0])),
|
|
"SNAPSHOT_RULES_UNSUPPORTED",
|
|
),
|
|
(
|
|
lambda snap, man: man.update({"unsupported_rule_count": 1}),
|
|
"SNAPSHOT_RULES_UNSUPPORTED",
|
|
),
|
|
],
|
|
)
|
|
def test_invalid_or_partial_grammar_is_never_clean(
|
|
tmp_path: Path, mutation, expected_reason: str
|
|
) -> None:
|
|
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
mutation(snapshot, manifest)
|
|
_rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == expected_reason
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"injected_value",
|
|
["grammar_profile", "normalizer_profile", "schema_version"],
|
|
)
|
|
def test_invalid_rule_text_cannot_inject_profile_failure_reason(
|
|
tmp_path: Path, injected_value: str
|
|
) -> None:
|
|
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
snapshot["rules"][0]["expression"] = {
|
|
"op": "regex",
|
|
"value": injected_value,
|
|
}
|
|
_rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.status == "degraded"
|
|
assert state.reason_code == "SNAPSHOT_RULES_UNSUPPORTED"
|
|
|
|
|
|
def test_semantically_duplicate_rule_is_rejected(tmp_path: Path) -> None:
|
|
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
duplicate = copy.deepcopy(snapshot["rules"][0])
|
|
duplicate["rule_id"] = "different_id_same_semantics"
|
|
snapshot["rules"].append(duplicate)
|
|
_rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
|
|
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
assert state.reason_code == "SNAPSHOT_RULES_UNSUPPORTED"
|
|
|
|
|
|
def test_near_gap_is_intervening_token_count_and_ordered() -> None:
|
|
rule = next(item for item in _bundle().rules if item.rule_id == "syn_near_ordered")
|
|
assert screening.evaluate_rule(rule, screening.tokenize("nebula x cabbage"))
|
|
assert not screening.evaluate_rule(rule, screening.tokenize("nebula x y cabbage"))
|
|
assert not screening.evaluate_rule(rule, screening.tokenize("cabbage x nebula"))
|
|
|
|
|
|
def test_near_never_crosses_context_segments() -> None:
|
|
report = screening.build_own_draft_report(
|
|
"nebula `x` cabbage",
|
|
artifact_id="segments.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert all(item["pattern_id"] != "syn_near_ordered" for item in report["matches"])
|
|
|
|
|
|
def test_explicit_timestamps_only_no_ambient_clock_source() -> None:
|
|
source = (SCRIPTS / "tortured_phrase_screening.py").read_text(encoding="utf-8")
|
|
assert ".now(" not in source
|
|
with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
|
|
screening.build_own_draft_report(
|
|
"luminous turnip",
|
|
artifact_id="draft.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at="today",
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def test_recorded_at_cannot_precede_checked_at() -> None:
|
|
with pytest.raises(screening.ScreeningError, match="must not precede"):
|
|
screening.build_own_draft_report(
|
|
"luminous turnip",
|
|
artifact_id="draft.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at="2026-08-10T01:00:01Z",
|
|
recorded_at="2026-08-10T01:00:00Z",
|
|
)
|
|
entry = _passport()["literature_corpus"][0]
|
|
with pytest.raises(screening.ScreeningError, match="must not precede"):
|
|
screening.build_cited_signal(
|
|
entry,
|
|
surface="cited_title",
|
|
state=_state(),
|
|
checked_at="2026-08-10T01:00:01Z",
|
|
recorded_at="2026-08-10T01:00:00Z",
|
|
)
|
|
|
|
|
|
def test_submicrosecond_timestamp_order_cannot_be_truncated() -> None:
|
|
checked_at = "2026-08-10T01:00:00.0000009Z"
|
|
recorded_at = "2026-08-10T01:00:00.0000001Z"
|
|
with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
|
|
screening.build_own_draft_report(
|
|
"luminous turnip",
|
|
artifact_id="draft.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=checked_at,
|
|
recorded_at=recorded_at,
|
|
)
|
|
with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
|
|
screening.build_cited_signal(
|
|
_passport()["literature_corpus"][0],
|
|
surface="cited_title",
|
|
state=_state(),
|
|
checked_at=checked_at,
|
|
recorded_at=recorded_at,
|
|
)
|
|
|
|
|
|
def test_renderer_is_one_page_bounded_and_has_claim_ceiling() -> None:
|
|
report = _report()
|
|
rendered = screening.render_own_draft_report(
|
|
report, _draft("own_draft.md"), state=_state()
|
|
)
|
|
assert screening.SUMMARY_LABEL in rendered
|
|
assert "does not establish papermill, AI, or author origin" in rendered
|
|
assert "not a clean-text certificate" in rendered
|
|
assert "one fixed page capped at 25" in rendered
|
|
assert "--all" not in rendered
|
|
|
|
|
|
def test_renderer_reports_omitted_count_and_machine_replay_key() -> None:
|
|
text = " ".join(["luminous turnip"] * 30)
|
|
report = screening.build_own_draft_report(
|
|
text,
|
|
artifact_id="many-matches.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert len(report["matches"]) > screening.MAX_RENDER_PAGE_SIZE
|
|
rendered = screening.render_own_draft_report(report, text, state=_state())
|
|
omitted = len(report["matches"]) - screening.MAX_RENDER_PAGE_SIZE
|
|
assert f"omitted {omitted}" in rendered
|
|
assert "Complete machine JSON replay key" in rendered
|
|
assert "artifact_id=many-matches.md" in rendered
|
|
assert report["report_sha256"] in rendered
|
|
|
|
|
|
def test_renderer_uses_outcome_specific_zero_and_unresolved_wording() -> None:
|
|
zero = screening.build_own_draft_report(
|
|
"ordinary unmatched prose",
|
|
artifact_id="zero.md",
|
|
artifact_format="markdown",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
rendered_zero = screening.render_own_draft_report(
|
|
zero, "ordinary unmatched prose", state=_state()
|
|
)
|
|
assert screening.SUMMARY_LABEL not in rendered_zero
|
|
assert "No phrase-list match observed on the checked surface" in rendered_zero
|
|
assert "not a clean-text certificate" in rendered_zero
|
|
|
|
unresolved_state = screening.snapshot_state(None, None)
|
|
unresolved = screening.build_own_draft_report(
|
|
"ordinary unmatched prose",
|
|
artifact_id="unresolved.md",
|
|
artifact_format="markdown",
|
|
state=unresolved_state,
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
rendered_unresolved = screening.render_own_draft_report(
|
|
unresolved, "ordinary unmatched prose", state=unresolved_state
|
|
)
|
|
assert screening.SUMMARY_LABEL not in rendered_unresolved
|
|
assert "screening is unresolved" in rendered_unresolved
|
|
|
|
|
|
def test_renderer_escapes_markdown_and_html_injection() -> None:
|
|
assert "<img" not in screening._markdown_cell("<img src=x>")
|
|
assert "\\[click\\]" in screening._markdown_cell("[click](https://example.invalid)")
|
|
|
|
|
|
def test_corpus_rows_are_per_surface_and_missing_abstract_is_explicit() -> None:
|
|
output = _enriched()
|
|
by_key = {item["citation_key"]: item for item in output["literature_corpus"]}
|
|
complete = by_key["fixture_complete_2026"]["bibliographic_integrity_signals"]
|
|
assert {item["tortured_phrase_context"]["surface"] for item in complete} == {
|
|
"cited_title",
|
|
"cited_abstract",
|
|
}
|
|
missing = by_key["fixture_missing_2026"]["bibliographic_integrity_signals"]
|
|
abstract = next(
|
|
item for item in missing if item["tortured_phrase_context"]["surface"] == "cited_abstract"
|
|
)
|
|
title = next(
|
|
item for item in missing if item["tortured_phrase_context"]["surface"] == "cited_title"
|
|
)
|
|
assert title["finding"] == "detected"
|
|
assert abstract["check_status"] == "not_checked"
|
|
assert abstract["finding"] == "unresolved"
|
|
assert abstract["tortured_phrase_context"]["reason_code"] == "ABSTRACT_MISSING"
|
|
assert abstract["tortured_phrase_context"]["counts"]["rules_evaluated"] == 0
|
|
|
|
|
|
def test_whitespace_abstract_is_explicitly_empty_not_checked() -> None:
|
|
document = _passport()
|
|
document["literature_corpus"][0]["abstract"] = " \t\n"
|
|
output = screening.enrich_passport(
|
|
document,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
abstract = next(
|
|
item
|
|
for item in output["literature_corpus"][0]["bibliographic_integrity_signals"]
|
|
if item["tortured_phrase_context"]["surface"] == "cited_abstract"
|
|
)
|
|
assert abstract["check_status"] == "not_checked"
|
|
assert abstract["finding"] == "unresolved"
|
|
assert abstract["tortured_phrase_context"]["reason_code"] == "ABSTRACT_EMPTY"
|
|
assert abstract["tortured_phrase_context"]["surface_binding"] == {
|
|
"content_sha256": None,
|
|
"content_utf8_bytes": None,
|
|
}
|
|
assert abstract["tortured_phrase_context"]["counts"]["rules_evaluated"] == 0
|
|
|
|
|
|
def test_legitimate_negative_only_means_no_configured_match() -> None:
|
|
output = _enriched()
|
|
entry = next(
|
|
item for item in output["literature_corpus"] if item["citation_key"] == "fixture_negative_2026"
|
|
)
|
|
assert all(
|
|
signal["finding"] == "not_detected"
|
|
for signal in entry["bibliographic_integrity_signals"]
|
|
)
|
|
assert all(
|
|
signal["tortured_phrase_context"]["boundary"]["absence_is_clean_certificate"]
|
|
is False
|
|
for signal in entry["bibliographic_integrity_signals"]
|
|
)
|
|
|
|
|
|
def test_manual_entries_are_not_exempt_and_pointer_is_not_dereferenced() -> None:
|
|
output = _enriched()
|
|
entry = output["literature_corpus"][0]
|
|
assert entry["obtained_via"] == "manual"
|
|
assert entry["source_pointer"].startswith("fixture://")
|
|
assert len(entry["bibliographic_integrity_signals"]) == 2
|
|
|
|
|
|
def test_enrichment_is_semantically_idempotent_and_supersedes_current_row() -> None:
|
|
first = _enriched()
|
|
second = screening.enrich_passport(
|
|
first,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
assert second == first
|
|
first["literature_corpus"][0]["title"] += " changed"
|
|
third = screening.enrich_passport(
|
|
first,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
signals = third["literature_corpus"][0]["bibliographic_integrity_signals"]
|
|
assert len([item for item in signals if item["schema_version"] == screening.SIGNAL_VERSION]) == 2
|
|
|
|
|
|
def test_enrichment_rejects_foreign_current_row_before_supersession() -> None:
|
|
document = _enriched()
|
|
row = document["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
row["subject"]["source_pointer"] = "fixture://foreign-source"
|
|
with pytest.raises(screening.ScreeningError, match="source_pointer does not belong"):
|
|
screening.enrich_passport(
|
|
document,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def test_legacy_and_unrelated_signals_are_preserved() -> None:
|
|
document = _passport()
|
|
legacy = json.loads(
|
|
(SCRIPTS / "fixtures/bibliographic_integrity_signals/tortured_phrase.json").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
legacy["subject"]["citation_key"] = document["literature_corpus"][0]["citation_key"]
|
|
legacy["subject"]["source_pointer"] = document["literature_corpus"][0]["source_pointer"]
|
|
document["literature_corpus"][0]["bibliographic_integrity_signals"] = [legacy]
|
|
output = screening.enrich_passport(
|
|
document,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
signals = output["literature_corpus"][0]["bibliographic_integrity_signals"]
|
|
assert signals[0] == legacy
|
|
assert len(signals) == 3
|
|
|
|
|
|
def test_multiple_current_rows_are_rejected() -> None:
|
|
document = _enriched()
|
|
entry = document["literature_corpus"][0]
|
|
duplicate = copy.deepcopy(entry["bibliographic_integrity_signals"][0])
|
|
context = duplicate["tortured_phrase_context"]
|
|
context["snapshot"]["snapshot_sha256"] = "0" * 64
|
|
context["snapshot"]["manifest_sha256"] = "1" * 64
|
|
duplicate["provenance"]["source_sha256"] = "0" * 64
|
|
duplicate["signal_id"] = screening._signal_id(
|
|
entry["citation_key"],
|
|
context["surface"],
|
|
context["snapshot"]["snapshot_sha256"],
|
|
context["surface_binding"]["content_sha256"],
|
|
)
|
|
for match in context["matches"]:
|
|
span = match["source_span"]
|
|
match["match_id"] = "tpm-" + screening._sha256_text(
|
|
screening._canonical_json(
|
|
{
|
|
"artifact_sha256": context["surface_binding"]["content_sha256"],
|
|
"snapshot_sha256": context["snapshot"]["snapshot_sha256"],
|
|
"surface": context["surface"],
|
|
"segment_id": match["segment_id"],
|
|
"context": match["context"],
|
|
"rule_id": match["pattern_id"],
|
|
"codepoint_start": span["codepoint_start"],
|
|
"codepoint_end": span["codepoint_end"],
|
|
}
|
|
)
|
|
)[:24]
|
|
entry["bibliographic_integrity_signals"].append(duplicate)
|
|
with pytest.raises(screening.ScreeningError, match="multiple current"):
|
|
screening.enrich_passport(
|
|
document,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def test_internally_invalid_current_row_is_not_silently_superseded() -> None:
|
|
document = _enriched()
|
|
row = document["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
row["signal_id"] = row["signal_id"][:-1] + (
|
|
"0" if row["signal_id"][-1] != "0" else "1"
|
|
)
|
|
with pytest.raises(screening.ScreeningError, match="internally inconsistent"):
|
|
screening.enrich_passport(
|
|
document,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def test_generated_id_collision_with_preserved_legacy_row_is_rejected() -> None:
|
|
document = _passport()
|
|
generated = screening.build_cited_signal(
|
|
document["literature_corpus"][0],
|
|
surface="cited_title",
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
legacy = json.loads(
|
|
(SCRIPTS / "fixtures/bibliographic_integrity_signals/tortured_phrase.json").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
legacy["signal_id"] = generated["signal_id"]
|
|
legacy["subject"]["citation_key"] = document["literature_corpus"][0]["citation_key"]
|
|
legacy["subject"]["source_pointer"] = document["literature_corpus"][0]["source_pointer"]
|
|
document["literature_corpus"][0]["bibliographic_integrity_signals"] = [legacy]
|
|
with pytest.raises(screening.ScreeningError, match="collides with preserved"):
|
|
screening.enrich_passport(
|
|
document,
|
|
state=_state(),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"title",
|
|
"source_pointer",
|
|
"signal_id",
|
|
"span",
|
|
"count",
|
|
"match_id",
|
|
"evidence",
|
|
],
|
|
)
|
|
def test_cited_binding_mutations_fail(mutation: str) -> None:
|
|
output = _enriched()
|
|
entry = output["literature_corpus"][0]
|
|
target_surface = "cited_abstract" if mutation == "span" else "cited_title"
|
|
signal = next(
|
|
item
|
|
for item in entry["bibliographic_integrity_signals"]
|
|
if item["tortured_phrase_context"]["surface"] == target_surface
|
|
)
|
|
if mutation == "title":
|
|
entry["title"] += " mutation"
|
|
elif mutation == "source_pointer":
|
|
entry["source_pointer"] += "/mutation"
|
|
elif mutation == "signal_id":
|
|
signal["signal_id"] = signal["signal_id"][:-1] + "0"
|
|
elif mutation == "span":
|
|
signal["tortured_phrase_context"]["matches"][0]["source_span"]["utf8_end"] -= 1
|
|
elif mutation == "count":
|
|
signal["tortured_phrase_context"]["counts"]["rule_match_count"] += 1
|
|
elif mutation == "match_id":
|
|
signal["tortured_phrase_context"]["matches"][0]["match_id"] = "tpm-" + "0" * 24
|
|
else:
|
|
signal["evidence"][0]["observed_value"] = "forged"
|
|
with pytest.raises(screening.ScreeningError):
|
|
screening.validate_cited_signal_binding(signal, entry)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"missing_abstract_as_checked",
|
|
"checked_with_zero_rules",
|
|
"checked_with_unknown_segment",
|
|
"match_context_crosses_surface",
|
|
"outer_not_checked_with_loaded_snapshot",
|
|
"not_checked_snapshot_keeps_hashes",
|
|
"match_limit_with_not_checked_snapshot",
|
|
],
|
|
)
|
|
def test_cited_state_and_surface_laundering_mutations_fail(mutation: str) -> None:
|
|
output = _enriched()
|
|
if mutation == "missing_abstract_as_checked":
|
|
entry = next(
|
|
item
|
|
for item in output["literature_corpus"]
|
|
if item["citation_key"] == "fixture_missing_2026"
|
|
)
|
|
signal = next(
|
|
item
|
|
for item in entry["bibliographic_integrity_signals"]
|
|
if item["tortured_phrase_context"]["surface"] == "cited_abstract"
|
|
)
|
|
signal["check_status"] = "checked"
|
|
signal["finding"] = "not_detected"
|
|
signal["provenance"]["checked_at"] = CHECKED_AT
|
|
signal["evidence"][0]["evidence_type"] = "list_record"
|
|
signal["evidence"][0]["observed_value"] = 0
|
|
signal["tortured_phrase_context"]["reason_code"] = "CHECK_COMPLETED"
|
|
else:
|
|
entry = output["literature_corpus"][0]
|
|
signal = next(
|
|
item
|
|
for item in entry["bibliographic_integrity_signals"]
|
|
if item["tortured_phrase_context"]["surface"] == "cited_title"
|
|
)
|
|
context = signal["tortured_phrase_context"]
|
|
if mutation == "checked_with_zero_rules":
|
|
context["counts"]["rules_evaluated"] = 0
|
|
elif mutation == "checked_with_unknown_segment":
|
|
context["counts"]["unknown_segments"] = 1
|
|
elif mutation == "match_context_crosses_surface":
|
|
match = context["matches"][0]
|
|
match["context"] = "cited_abstract"
|
|
payload = {
|
|
"artifact_sha256": context["surface_binding"]["content_sha256"],
|
|
"snapshot_sha256": context["snapshot"]["snapshot_sha256"],
|
|
"surface": context["surface"],
|
|
"segment_id": match["segment_id"],
|
|
"context": match["context"],
|
|
"rule_id": match["pattern_id"],
|
|
"codepoint_start": match["source_span"]["codepoint_start"],
|
|
"codepoint_end": match["source_span"]["codepoint_end"],
|
|
}
|
|
match["match_id"] = "tpm-" + screening._sha256_text(
|
|
screening._canonical_json(payload)
|
|
)[:24]
|
|
context["counts"]["matches_by_context"]["cited_title"] -= 1
|
|
context["counts"]["matches_by_context"]["cited_abstract"] += 1
|
|
elif mutation in {
|
|
"outer_not_checked_with_loaded_snapshot",
|
|
"not_checked_snapshot_keeps_hashes",
|
|
}:
|
|
signal["check_status"] = "not_checked"
|
|
signal["finding"] = "unresolved"
|
|
signal["provenance"]["checked_at"] = None
|
|
signal["evidence"][0]["evidence_type"] = "degradation_record"
|
|
signal["evidence"][0]["observed_value"] = "SNAPSHOT_NOT_PROVIDED"
|
|
context["reason_code"] = "SNAPSHOT_NOT_PROVIDED"
|
|
context["counts"] = screening._empty_counts()
|
|
context["matches"] = []
|
|
if mutation == "not_checked_snapshot_keeps_hashes":
|
|
context["snapshot"] = screening._snapshot_binding(
|
|
screening.snapshot_state(None, None)
|
|
)
|
|
context["snapshot"]["snapshot_sha256"] = "0" * 64
|
|
context["snapshot"]["manifest_sha256"] = "1" * 64
|
|
signal["signal_id"] = screening._signal_id(
|
|
entry["citation_key"],
|
|
context["surface"],
|
|
context["snapshot"]["snapshot_sha256"],
|
|
context["surface_binding"]["content_sha256"],
|
|
)
|
|
signal["provenance"]["source_name"] = (
|
|
"tortured-phrase snapshot unavailable"
|
|
)
|
|
signal["provenance"]["source_version"] = None
|
|
signal["provenance"]["source_sha256"] = "0" * 64
|
|
else:
|
|
signal["check_status"] = "degraded"
|
|
signal["finding"] = "unresolved"
|
|
signal["evidence"][0]["evidence_type"] = "degradation_record"
|
|
signal["evidence"][0]["observed_value"] = "MATCH_RESOURCE_LIMIT"
|
|
context["reason_code"] = "MATCH_RESOURCE_LIMIT"
|
|
context["counts"] = screening._empty_counts()
|
|
context["matches"] = []
|
|
context["snapshot"] = screening._snapshot_binding(
|
|
screening.snapshot_state(None, None)
|
|
)
|
|
signal["provenance"]["source_name"] = (
|
|
"tortured-phrase snapshot unavailable"
|
|
)
|
|
signal["provenance"]["source_version"] = None
|
|
signal["provenance"]["source_sha256"] = None
|
|
with pytest.raises(screening.ScreeningError):
|
|
screening.validate_cited_signal_binding(signal, entry)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"rights",
|
|
[
|
|
{
|
|
"basis": "user_declared_authorized",
|
|
"redistribution_status": "permitted",
|
|
"reference": None,
|
|
"user_declaration": None,
|
|
},
|
|
{
|
|
"basis": "written_permission",
|
|
"redistribution_status": "permitted",
|
|
"reference": None,
|
|
"user_declaration": None,
|
|
},
|
|
{
|
|
"basis": "unresolved",
|
|
"redistribution_status": "permitted",
|
|
"reference": None,
|
|
"user_declaration": None,
|
|
},
|
|
],
|
|
)
|
|
def test_cited_carrier_rejects_impossible_rights_projection(rights: dict) -> None:
|
|
output = _enriched()
|
|
entry = output["literature_corpus"][0]
|
|
signal = entry["bibliographic_integrity_signals"][0]
|
|
signal["tortured_phrase_context"]["snapshot"]["rights"] = rights
|
|
with pytest.raises(screening.ScreeningError):
|
|
screening.validate_cited_signal_binding(signal, entry)
|
|
|
|
|
|
def test_cited_carrier_rejects_missing_loaded_source_locator() -> None:
|
|
output = _enriched()
|
|
entry = output["literature_corpus"][0]
|
|
signal = entry["bibliographic_integrity_signals"][0]
|
|
signal["tortured_phrase_context"]["snapshot"]["source"]["locator"] = None
|
|
with pytest.raises(screening.ScreeningError):
|
|
screening.validate_cited_signal_binding(signal, entry)
|
|
|
|
|
|
def test_tortured_phrase_cannot_be_deterministic_or_terminal() -> None:
|
|
schema = screening._load_schema(screening.SIGNAL_SCHEMA_PATH)
|
|
validator = Draft202012Validator(
|
|
schema, format_checker=Draft202012Validator.FORMAT_CHECKER
|
|
)
|
|
signal = _enriched()["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
assert not list(validator.iter_errors(signal))
|
|
signal["epistemic_class"] = "deterministic_fact"
|
|
signal["epistemic_label"] = "RESOLVER-OR-LIST-OBSERVATION"
|
|
assert list(validator.iter_errors(signal))
|
|
signal = _enriched()["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
signal["terminal_policy"] = {
|
|
"eligible": True,
|
|
"owner": "citation_finalizer",
|
|
"policy_key": "tortured_phrase",
|
|
"current_effect": "policy_gated",
|
|
}
|
|
assert list(validator.iter_errors(signal))
|
|
|
|
|
|
def test_report_never_copies_raw_pattern_bodies() -> None:
|
|
report_bytes = json.dumps(_report(), ensure_ascii=False)
|
|
snapshot = json.loads((FIXTURES / "snapshot.json").read_text(encoding="utf-8"))
|
|
for rule in snapshot["rules"]:
|
|
expression = json.dumps(rule["expression"], ensure_ascii=False, sort_keys=True)
|
|
assert expression not in report_bytes
|
|
|
|
|
|
def test_cli_rejects_in_place_passport_output() -> None:
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"enrich-passport",
|
|
"--input",
|
|
str(FIXTURES / "corpus_input.yaml"),
|
|
"--output",
|
|
str(FIXTURES / "corpus_input.yaml"),
|
|
"--snapshot",
|
|
str(FIXTURES / "snapshot.json"),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
assert "refuses in-place" in result.stderr
|
|
|
|
|
|
@pytest.mark.parametrize("alias", ["snapshot", "manifest", "snapshot_hardlink"])
|
|
def test_scan_output_cannot_alias_any_named_input(
|
|
tmp_path: Path, alias: str
|
|
) -> None:
|
|
draft = tmp_path / "draft.md"
|
|
snapshot = tmp_path / "snapshot.json"
|
|
manifest = tmp_path / "manifest.json"
|
|
draft.write_bytes((FIXTURES / "own_draft.md").read_bytes())
|
|
snapshot.write_bytes((FIXTURES / "snapshot.json").read_bytes())
|
|
manifest.write_bytes((FIXTURES / "snapshot_manifest.json").read_bytes())
|
|
if alias == "snapshot":
|
|
output = snapshot
|
|
elif alias == "manifest":
|
|
output = manifest
|
|
else:
|
|
output = tmp_path / "snapshot-hardlink.json"
|
|
output.hardlink_to(snapshot)
|
|
snapshot_before = snapshot.read_bytes()
|
|
manifest_before = manifest.read_bytes()
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"scan-draft",
|
|
"--input",
|
|
str(draft),
|
|
"--artifact-id",
|
|
"draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--snapshot",
|
|
str(snapshot),
|
|
"--snapshot-manifest",
|
|
str(manifest),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
"--output",
|
|
str(output),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
assert "named-input alias" in result.stderr
|
|
assert snapshot.read_bytes() == snapshot_before
|
|
assert manifest.read_bytes() == manifest_before
|
|
|
|
|
|
def test_corpus_output_match_cap_rejects_before_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, "MAX_CORPUS_OUTPUT_MATCHES", 0)
|
|
with pytest.raises(screening.MatchLimitError, match="corpus output match count"):
|
|
_enriched()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("limit_name", "document", "message"),
|
|
[
|
|
(
|
|
"MAX_CORPUS_ENTRIES",
|
|
{"literature_corpus": [{}, {}]},
|
|
"more than 1 entries",
|
|
),
|
|
(
|
|
"MAX_CORPUS_EXISTING_SIGNALS",
|
|
{"literature_corpus": [{"bibliographic_integrity_signals": [{}]}]},
|
|
"exceed 0 rows",
|
|
),
|
|
],
|
|
)
|
|
def test_corpus_cardinality_caps_fail_before_copy_or_row_construction(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
limit_name: str,
|
|
document: dict,
|
|
message: str,
|
|
) -> None:
|
|
monkeypatch.setattr(screening, limit_name, 1 if limit_name.endswith("ENTRIES") else 0)
|
|
|
|
def unexpected_copy(_value):
|
|
raise AssertionError("copy must not run after an over-limit input")
|
|
|
|
monkeypatch.setattr(screening.copy, "deepcopy", unexpected_copy)
|
|
with pytest.raises(screening.MatchLimitError, match=message):
|
|
screening.enrich_passport(
|
|
document,
|
|
state=screening.snapshot_state(None, None),
|
|
checked_at=CHECKED_AT,
|
|
recorded_at=RECORDED_AT,
|
|
)
|
|
|
|
|
|
def test_deep_json_passport_fails_closed_without_traceback_or_output_mutation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
passport = tmp_path / "deep-passport.json"
|
|
passport.write_text(
|
|
'{"literature_corpus":[],"extension":'
|
|
+ "[" * 1000
|
|
+ "0"
|
|
+ "]" * 1000
|
|
+ "}\n",
|
|
encoding="utf-8",
|
|
)
|
|
output = tmp_path / "output.json"
|
|
output.write_bytes(b"sentinel\n")
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"enrich-passport",
|
|
"--input",
|
|
str(passport),
|
|
"--output",
|
|
str(output),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
assert "Traceback" not in result.stderr
|
|
assert "strict JSON" in result.stderr or "structure exceeds" in result.stderr
|
|
assert output.read_bytes() == b"sentinel\n"
|
|
|
|
|
|
def test_deep_advisory_report_fails_closed_without_traceback(tmp_path: Path) -> None:
|
|
report = tmp_path / "deep-report.json"
|
|
report.write_text(
|
|
'{"extension":' + "[" * 1000 + "0" + "]" * 1000 + "}\n",
|
|
encoding="utf-8",
|
|
)
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"validate-draft",
|
|
"--input",
|
|
str(FIXTURES / "own_draft.md"),
|
|
"--artifact-id",
|
|
"own_draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--snapshot",
|
|
str(FIXTURES / "snapshot.json"),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--report",
|
|
str(report),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
assert "Traceback" not in result.stderr
|
|
assert "strict JSON" in result.stderr or "structure exceeds" in result.stderr
|
|
|
|
|
|
@pytest.mark.parametrize("cwd", [REPO_ROOT, SCRIPTS])
|
|
def test_runtime_imports_in_package_and_direct_script_modes(cwd: Path) -> None:
|
|
module = "scripts.tortured_phrase_screening" if cwd == REPO_ROOT else "tortured_phrase_screening"
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", f"import {module}"],
|
|
cwd=cwd,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_package_import_cannot_bind_shadow_bibliographic_module(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
(tmp_path / "bibliographic_integrity_signals.py").write_text(
|
|
"raise RuntimeError('shadow module imported')\n", encoding="utf-8"
|
|
)
|
|
code = (
|
|
"import sys; "
|
|
f"sys.path.insert(0, {str(tmp_path)!r}); "
|
|
"import scripts.tortured_phrase_screening as module; "
|
|
"print(module._validate_existing_phrase_projection.__module__); "
|
|
"print(module._validate_existing_phrase_projection.__code__.co_filename)"
|
|
)
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", code],
|
|
cwd=REPO_ROOT,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
lines = result.stdout.splitlines()
|
|
assert lines[0] == "scripts.bibliographic_integrity_signals"
|
|
assert Path(lines[1]).resolve() == (
|
|
SCRIPTS / "bibliographic_integrity_signals.py"
|
|
).resolve()
|
|
|
|
|
|
def test_yaml_aliases_fail_closed_without_output(tmp_path: Path) -> None:
|
|
passport = tmp_path / "aliased.yaml"
|
|
passport.write_text(
|
|
"shared: &rows []\nliterature_corpus: *rows\n",
|
|
encoding="utf-8",
|
|
)
|
|
output = tmp_path / "output.yaml"
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"enrich-passport",
|
|
"--input",
|
|
str(passport),
|
|
"--output",
|
|
str(output),
|
|
"--snapshot",
|
|
str(FIXTURES / "snapshot.json"),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
assert "YAML aliases are forbidden" in result.stderr
|
|
assert not output.exists()
|
|
|
|
|
|
def test_cli_has_no_all_or_page_traversal() -> None:
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"render-draft",
|
|
"--help",
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0
|
|
assert "--all" not in result.stdout
|
|
assert "--page" not in result.stdout
|
|
|
|
|
|
def test_invalid_snapshot_cli_writes_degraded_artifact_and_returns_one(tmp_path: Path) -> None:
|
|
bad = tmp_path / "bad.json"
|
|
bad.write_text("{}\n", encoding="utf-8")
|
|
output = tmp_path / "report.json"
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"scan-draft",
|
|
"--input",
|
|
str(FIXTURES / "own_draft.md"),
|
|
"--artifact-id",
|
|
"own_draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--snapshot",
|
|
str(bad),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
"--output",
|
|
str(output),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
report = json.loads(output.read_text(encoding="utf-8"))
|
|
assert report["check_status"] == "degraded"
|
|
assert report["finding"] == "unresolved"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("text", "reason"),
|
|
[
|
|
("", "DOCUMENT_EMPTY"),
|
|
("`unclosed luminous turnip", "DOCUMENT_PARSE_DEGRADED"),
|
|
],
|
|
)
|
|
def test_loaded_snapshot_scan_returns_one_for_degraded_artifact(
|
|
tmp_path: Path, text: str, reason: str
|
|
) -> None:
|
|
draft = tmp_path / "draft.md"
|
|
draft.write_text(text, encoding="utf-8")
|
|
output = tmp_path / "report.json"
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"scan-draft",
|
|
"--input",
|
|
str(draft),
|
|
"--artifact-id",
|
|
"draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--snapshot",
|
|
str(FIXTURES / "snapshot.json"),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
"--output",
|
|
str(output),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
report = json.loads(output.read_text(encoding="utf-8"))
|
|
assert report["check_status"] == "degraded"
|
|
assert report["reason_code"] == reason
|
|
|
|
|
|
def test_loaded_snapshot_resource_degradation_returns_one_after_write(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
draft = tmp_path / "draft.md"
|
|
draft.write_text("alpha\n\nbeta", encoding="utf-8")
|
|
output = tmp_path / "report.json"
|
|
monkeypatch.setattr(screening, "MAX_SEGMENTS", 1)
|
|
result = screening.main(
|
|
[
|
|
"scan-draft",
|
|
"--input",
|
|
str(draft),
|
|
"--artifact-id",
|
|
"draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--snapshot",
|
|
str(FIXTURES / "snapshot.json"),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
"--output",
|
|
str(output),
|
|
]
|
|
)
|
|
assert result == 1
|
|
report = json.loads(output.read_text(encoding="utf-8"))
|
|
assert report["check_status"] == "degraded"
|
|
assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
|
|
|
|
|
|
def test_loaded_snapshot_corpus_degradation_returns_one_after_write(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
passport = tmp_path / "corpus.yaml"
|
|
passport.write_bytes((FIXTURES / "corpus_input.yaml").read_bytes())
|
|
output = tmp_path / "output.yaml"
|
|
monkeypatch.setattr(screening, "MAX_MATCH_WORK_UNITS", 0)
|
|
result = screening.main(
|
|
[
|
|
"enrich-passport",
|
|
"--input",
|
|
str(passport),
|
|
"--output",
|
|
str(output),
|
|
"--snapshot",
|
|
str(FIXTURES / "snapshot.json"),
|
|
"--snapshot-manifest",
|
|
str(FIXTURES / "snapshot_manifest.json"),
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
]
|
|
)
|
|
assert result == 1
|
|
enriched, _kind = screening._load_passport(output)
|
|
assert any(
|
|
signal.get("check_status") == "degraded"
|
|
for entry in enriched["literature_corpus"]
|
|
for signal in entry["bibliographic_integrity_signals"]
|
|
if signal.get("schema_version") == screening.SIGNAL_VERSION
|
|
)
|
|
|
|
|
|
def test_explicit_not_checked_scan_remains_successful(tmp_path: Path) -> None:
|
|
output = tmp_path / "report.json"
|
|
result = screening.main(
|
|
[
|
|
"scan-draft",
|
|
"--input",
|
|
str(FIXTURES / "own_draft.md"),
|
|
"--artifact-id",
|
|
"own_draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--checked-at",
|
|
CHECKED_AT,
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
"--output",
|
|
str(output),
|
|
]
|
|
)
|
|
assert result == 0
|
|
assert json.loads(output.read_text(encoding="utf-8"))["check_status"] == (
|
|
"not_checked"
|
|
)
|
|
|
|
|
|
def test_failed_scan_does_not_overwrite_input_or_partial_output(tmp_path: Path) -> None:
|
|
draft = tmp_path / "draft.md"
|
|
draft.write_text("luminous turnip", encoding="utf-8")
|
|
before = draft.read_bytes()
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
"scan-draft",
|
|
"--input",
|
|
str(draft),
|
|
"--artifact-id",
|
|
"draft.md",
|
|
"--format",
|
|
"markdown",
|
|
"--checked-at",
|
|
"bad-time",
|
|
"--recorded-at",
|
|
RECORDED_AT,
|
|
"--output",
|
|
str(tmp_path / "report.json"),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 1
|
|
assert draft.read_bytes() == before
|
|
assert not (tmp_path / "report.json").exists()
|