psyclaw 0.27.23 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/agents/recommended/catalog.json +99 -0
- package/dist/apps/panel/index.html +5 -5
- package/dist/src/adapters/pi/extension.js +339 -94
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/adapters/pi/rpc.d.ts +2 -0
- package/dist/src/adapters/pi/rpc.js +9 -2
- package/dist/src/adapters/pi/rpc.js.map +1 -1
- package/dist/src/agents/import.js +44 -32
- package/dist/src/agents/import.js.map +1 -1
- package/dist/src/agents/recommended-personas.d.ts +42 -0
- package/dist/src/agents/recommended-personas.js +135 -0
- package/dist/src/agents/recommended-personas.js.map +1 -0
- package/dist/src/analysis/hooks.d.ts +1 -0
- package/dist/src/analysis/hooks.js +20 -1
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/ars/bridge.d.ts +20 -0
- package/dist/src/ars/bridge.js +132 -0
- package/dist/src/ars/bridge.js.map +1 -0
- package/dist/src/ars/contracts.d.ts +55 -0
- package/dist/src/ars/contracts.js +2 -0
- package/dist/src/ars/contracts.js.map +1 -0
- package/dist/src/ars/panel-plan.d.ts +4 -0
- package/dist/src/ars/panel-plan.js +11 -0
- package/dist/src/ars/panel-plan.js.map +1 -0
- package/dist/src/ars/pi-panel-executor.d.ts +30 -0
- package/dist/src/ars/pi-panel-executor.js +140 -0
- package/dist/src/ars/pi-panel-executor.js.map +1 -0
- package/dist/src/ars/profile.d.ts +36 -0
- package/dist/src/ars/profile.js +119 -0
- package/dist/src/ars/profile.js.map +1 -0
- package/dist/src/ars/re-review.d.ts +22 -0
- package/dist/src/ars/re-review.js +172 -0
- package/dist/src/ars/re-review.js.map +1 -0
- package/dist/src/branding.d.ts +2 -4
- package/dist/src/branding.js +3 -5
- package/dist/src/branding.js.map +1 -1
- package/dist/src/bundled-tools.d.ts +3 -0
- package/dist/src/bundled-tools.js +21 -0
- package/dist/src/bundled-tools.js.map +1 -0
- package/dist/src/chat.js +6 -5
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +6 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/creation/contracts.d.ts +41 -0
- package/dist/src/creation/contracts.js +2 -0
- package/dist/src/creation/contracts.js.map +1 -0
- package/dist/src/creation/service.d.ts +7 -0
- package/dist/src/creation/service.js +208 -0
- package/dist/src/creation/service.js.map +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/installer.js +32 -10
- package/dist/src/install/installer.js.map +1 -1
- package/dist/src/orchestration/bundled-personas.d.ts +16 -0
- package/dist/src/orchestration/bundled-personas.js +101 -0
- package/dist/src/orchestration/bundled-personas.js.map +1 -0
- package/dist/src/orchestration/effects.d.ts +8 -0
- package/dist/src/orchestration/effects.js +39 -0
- package/dist/src/orchestration/effects.js.map +1 -0
- package/dist/src/orchestration/personas.d.ts +16 -0
- package/dist/src/orchestration/personas.js +64 -0
- package/dist/src/orchestration/personas.js.map +1 -0
- package/dist/src/orchestration/pi-executor.d.ts +9 -4
- package/dist/src/orchestration/pi-executor.js +55 -22
- package/dist/src/orchestration/pi-executor.js.map +1 -1
- package/dist/src/orchestration/research-agents.d.ts +3 -7
- package/dist/src/orchestration/research-agents.js +5 -69
- package/dist/src/orchestration/research-agents.js.map +1 -1
- package/dist/src/panel/server.js +19 -4
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/paths.d.ts +3 -0
- package/dist/src/project/paths.js +7 -0
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/rules/user-rules.d.ts +8 -0
- package/dist/src/rules/user-rules.js +36 -0
- package/dist/src/rules/user-rules.js.map +1 -0
- package/dist/src/skills/contracts.d.ts +4 -4
- package/dist/src/skills/recommended.js +1 -1
- package/dist/src/skills/registry.js +49 -28
- package/dist/src/skills/registry.js.map +1 -1
- package/dist/src/style/cli-ui.d.ts +14 -0
- package/dist/src/style/cli-ui.js +45 -1
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/tui/skill-manager.js +1 -1
- package/dist/src/tui/skill-manager.js.map +1 -1
- package/dist/src/updates/update.js +9 -3
- package/dist/src/updates/update.js.map +1 -1
- package/package.json +15 -3
- package/scripts/rebrand-pi.mjs +6 -0
- package/skills/recommended/catalog.json +2 -11
- package/vendor/ars/.claude/CLAUDE.md +371 -0
- package/vendor/ars/.command-invariants.toml +24 -0
- package/vendor/ars/CITATION.cff +35 -0
- package/vendor/ars/LICENSE +417 -0
- package/vendor/ars/MODE_REGISTRY.md +76 -0
- package/vendor/ars/NOTICE.md +26 -0
- package/vendor/ars/POSITIONING.md +99 -0
- package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
- package/vendor/ars/README.md +751 -0
- package/vendor/ars/SECURITY.md +52 -0
- package/vendor/ars/THIRD_PARTY.md +70 -0
- package/vendor/ars/academic-paper/SKILL.md +542 -0
- package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
- package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
- package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
- package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
- package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
- package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
- package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
- package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
- package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
- package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
- package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
- package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
- package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
- package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
- package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
- package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
- package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
- package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
- package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
- package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
- package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
- package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
- package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
- package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
- package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
- package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
- package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
- package/vendor/ars/academic-paper/references/changelog.md +11 -0
- package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
- package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
- package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
- package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
- package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
- package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
- package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
- package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
- package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
- package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
- package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
- package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
- package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
- package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
- package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
- package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
- package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
- package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
- package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
- package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
- package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
- package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
- package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
- package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
- package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
- package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
- package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
- package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
- package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
- package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
- package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
- package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
- package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
- package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
- package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
- package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
- package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
- package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
- package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
- package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
- package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
- package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
- package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
- package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
- package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
- package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
- package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
- package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
- package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
- package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
- package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
- package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
- package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
- package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
- package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
- package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
- package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
- package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
- package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
- package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
- package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
- package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
- package/vendor/ars/academic-pipeline/SKILL.md +736 -0
- package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
- package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
- package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
- package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
- package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
- package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
- package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
- package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
- package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
- package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
- package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
- package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
- package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
- package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
- package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
- package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
- package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
- package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
- package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
- package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
- package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
- package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
- package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
- package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
- package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
- package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
- package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
- package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
- package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
- package/vendor/ars/agents/report_compiler_agent.md +341 -0
- package/vendor/ars/agents/research_architect_agent.md +298 -0
- package/vendor/ars/agents/synthesis_agent.md +356 -0
- package/vendor/ars/commands/ars-3w.md +10 -0
- package/vendor/ars/commands/ars-abstract.md +10 -0
- package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
- package/vendor/ars/commands/ars-citation-check.md +10 -0
- package/vendor/ars/commands/ars-disclosure.md +10 -0
- package/vendor/ars/commands/ars-format-convert.md +10 -0
- package/vendor/ars/commands/ars-full.md +9 -0
- package/vendor/ars/commands/ars-lit-review.md +12 -0
- package/vendor/ars/commands/ars-mark-read.md +18 -0
- package/vendor/ars/commands/ars-outline.md +10 -0
- package/vendor/ars/commands/ars-plan.md +10 -0
- package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
- package/vendor/ars/commands/ars-reviewer.md +9 -0
- package/vendor/ars/commands/ars-revision-coach.md +9 -0
- package/vendor/ars/commands/ars-revision.md +10 -0
- package/vendor/ars/commands/ars-unmark-read.md +16 -0
- package/vendor/ars/deep-research/SKILL.md +600 -0
- package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
- package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
- package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
- package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
- package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
- package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
- package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
- package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
- package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
- package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
- package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
- package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
- package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
- package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
- package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
- package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
- package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
- package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
- package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
- package/vendor/ars/deep-research/examples/review_mode.md +253 -0
- package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
- package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
- package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
- package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
- package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
- package/vendor/ars/deep-research/references/changelog.md +22 -0
- package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
- package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
- package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
- package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
- package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
- package/vendor/ars/deep-research/references/failure_paths.md +355 -0
- package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
- package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
- package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
- package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
- package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
- package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
- package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
- package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
- package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
- package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
- package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
- package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
- package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
- package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
- package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
- package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
- package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
- package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
- package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
- package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
- package/vendor/ars/package.json +24 -0
- package/vendor/ars/pi/README.md +161 -0
- package/vendor/ars/pi/package.json +26 -0
- package/vendor/ars/pi/wrapper.js +193 -0
- package/vendor/ars/pi/wrapper.test.mjs +201 -0
- package/vendor/ars/pyproject.toml +2 -0
- package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
- package/vendor/ars/scripts/_block_parser.py +396 -0
- package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
- package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
- package/vendor/ars/scripts/_e4_evidence.py +110 -0
- package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
- package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
- package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
- package/vendor/ars/scripts/_passport_yaml.py +53 -0
- package/vendor/ars/scripts/_skill_lint.py +254 -0
- package/vendor/ars/scripts/_text_similarity.py +141 -0
- package/vendor/ars/scripts/adapters/README.md +89 -0
- package/vendor/ars/scripts/adapters/_common.py +209 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture//344/270/255/346/226/207/346/252/224/345/220/215_2024.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
- package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
- package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
- package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
- package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
- package/vendor/ars/scripts/adapters/obsidian.py +336 -0
- package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
- package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
- package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
- package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
- package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
- package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
- package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
- package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
- package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
- package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
- package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
- package/vendor/ars/scripts/adapters/zotero.py +318 -0
- package/vendor/ars/scripts/adjudication_activity.py +1592 -0
- package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
- package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
- package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
- package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
- package/vendor/ars/scripts/ars_mark_read.py +521 -0
- package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
- package/vendor/ars/scripts/ars_update_check.sh +215 -0
- package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
- package/vendor/ars/scripts/arxiv_client.py +222 -0
- package/vendor/ars/scripts/audit_snapshot.py +572 -0
- package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
- package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
- package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
- package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
- package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
- package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
- package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
- package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
- package/vendor/ars/scripts/check_215_field_norm.py +173 -0
- package/vendor/ars/scripts/check_216_surface_form.py +250 -0
- package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
- package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
- package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
- package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
- package/vendor/ars/scripts/check_439_format_profile.py +307 -0
- package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
- package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
- package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
- package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
- package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
- package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
- package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
- package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
- package/vendor/ars/scripts/check_benchmark_report.py +79 -0
- package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
- package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
- package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
- package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
- package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
- package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
- package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
- package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
- package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
- package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
- package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
- package/vendor/ars/scripts/check_compliance_report.py +108 -0
- package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
- package/vendor/ars/scripts/check_control_availability.py +172 -0
- package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
- package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
- package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
- package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
- package/vendor/ars/scripts/check_data_access_level.py +131 -0
- package/vendor/ars/scripts/check_data_flows.py +252 -0
- package/vendor/ars/scripts/check_decision_contract.py +464 -0
- package/vendor/ars/scripts/check_degradation_registry.py +326 -0
- package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
- package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
- package/vendor/ars/scripts/check_e4_promotion.py +195 -0
- package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
- package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
- package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
- package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
- package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
- package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
- package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
- package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
- package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
- package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
- package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
- package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
- package/vendor/ars/scripts/check_model_tiering.py +223 -0
- package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
- package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
- package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
- package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
- package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
- package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
- package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
- package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
- package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
- package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
- package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
- package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
- package/vendor/ars/scripts/check_ranking_lift.py +323 -0
- package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
- package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
- package/vendor/ars/scripts/check_repro_lock.py +85 -0
- package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
- package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
- package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
- package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
- package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
- package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
- package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
- package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
- package/vendor/ars/scripts/check_risk_register.py +280 -0
- package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
- package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
- package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
- package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
- package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
- package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
- package/vendor/ars/scripts/check_sprint_contract.py +371 -0
- package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
- package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
- package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
- package/vendor/ars/scripts/check_task_type.py +22 -0
- package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
- package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
- package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
- package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
- package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
- package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
- package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
- package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
- package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
- package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
- package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
- package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
- package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
- package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
- package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
- package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
- package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
- package/vendor/ars/scripts/check_version_consistency.py +826 -0
- package/vendor/ars/scripts/check_workflow_classification.py +223 -0
- package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
- package/vendor/ars/scripts/citation_verification_summary.py +85 -0
- package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
- package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
- package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
- package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
- package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
- package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
- package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
- package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
- package/vendor/ars/scripts/contamination_signals.py +689 -0
- package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
- package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
- package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
- package/vendor/ars/scripts/cross_model_handoff.py +359 -0
- package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
- package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
- package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
- package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
- package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
- package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
- package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
- package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
- package/vendor/ars/scripts/crossref_client.py +225 -0
- package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
- package/vendor/ars/scripts/evidence_rows.py +2043 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
- package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
- package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
- package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
- package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
- package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
- package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
- package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
- package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
- package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
- package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
- package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
- package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
- package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
- package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
- package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
- package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
- package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
- package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
- package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
- package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
- package/vendor/ars/scripts/openalex_client.py +232 -0
- package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
- package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
- package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
- package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
- package/vendor/ars/scripts/recompute_receipts.py +1414 -0
- package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
- package/vendor/ars/scripts/render_eval_comment.py +130 -0
- package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
- package/vendor/ars/scripts/repro_lock_validation.py +90 -0
- package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
- package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
- package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
- package/vendor/ars/scripts/resume_e4_record.py +509 -0
- package/vendor/ars/scripts/retraction_status.py +484 -0
- package/vendor/ars/scripts/review_criteria_binding.py +889 -0
- package/vendor/ars/scripts/review_panel_provenance.py +744 -0
- package/vendor/ars/scripts/revision_roadmap.py +1967 -0
- package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
- package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
- package/vendor/ars/scripts/run_evals.py +513 -0
- package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
- package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
- package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
- package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
- package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
- package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
- package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
- package/vendor/ars/scripts/slr_lineage.py +59 -0
- package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
- package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
- package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
- package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
- package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
- package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
- package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
- package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
- package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
- package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
- package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
- package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
- package/vendor/ars/scripts/test_ars_update_check.py +816 -0
- package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
- package/vendor/ars/scripts/test_arxiv_client.py +374 -0
- package/vendor/ars/scripts/test_audit_schemas.py +560 -0
- package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
- package/vendor/ars/scripts/test_block_parser.py +259 -0
- package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
- package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
- package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
- package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
- package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
- package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
- package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
- package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
- package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
- package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
- package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
- package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
- package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
- package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
- package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
- package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
- package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
- package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
- package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
- package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
- package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
- package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
- package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
- package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
- package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
- package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
- package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
- package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
- package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
- package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
- package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
- package/vendor/ars/scripts/test_check_control_availability.py +361 -0
- package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
- package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
- package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
- package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
- package/vendor/ars/scripts/test_check_data_flows.py +382 -0
- package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
- package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
- package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
- package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
- package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
- package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
- package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
- package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
- package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
- package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
- package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
- package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
- package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
- package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
- package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
- package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
- package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
- package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
- package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
- package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
- package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
- package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
- package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
- package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
- package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
- package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
- package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
- package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
- package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
- package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
- package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
- package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
- package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
- package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
- package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
- package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
- package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
- package/vendor/ars/scripts/test_check_risk_register.py +336 -0
- package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
- package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
- package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
- package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
- package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
- package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
- package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
- package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
- package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
- package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
- package/vendor/ars/scripts/test_check_task_type.py +116 -0
- package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
- package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
- package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
- package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
- package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
- package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
- package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
- package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
- package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
- package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
- package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
- package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
- package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
- package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
- package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
- package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
- package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
- package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
- package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
- package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
- package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
- package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
- package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
- package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
- package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
- package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
- package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
- package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
- package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
- package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
- package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
- package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
- package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
- package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
- package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
- package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
- package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
- package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
- package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
- package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
- package/vendor/ars/scripts/test_crossref_client.py +393 -0
- package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
- package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
- package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
- package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
- package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
- package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
- package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
- package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
- package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
- package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
- package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
- package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
- package/vendor/ars/scripts/test_openalex_client.py +490 -0
- package/vendor/ars/scripts/test_passport_yaml.py +104 -0
- package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
- package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
- package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
- package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
- package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
- package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
- package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
- package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
- package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
- package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
- package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
- package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
- package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
- package/vendor/ars/scripts/test_retraction_status.py +456 -0
- package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
- package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
- package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
- package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
- package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
- package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
- package/vendor/ars/scripts/test_run_evals.py +430 -0
- package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
- package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
- package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
- package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
- package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
- package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
- package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
- package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
- package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
- package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
- package/vendor/ars/scripts/test_text_similarity.py +95 -0
- package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
- package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
- package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
- package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
- package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
- package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
- package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
- package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
- package/vendor/ars/scripts/test_verification_cache.py +280 -0
- package/vendor/ars/scripts/test_verification_gate.py +461 -0
- package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
- package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
- package/vendor/ars/scripts/test_version_records_schema.py +211 -0
- package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
- package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
- package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
- package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
- package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
- package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
- package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
- package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
- package/vendor/ars/scripts/verification_cache.py +276 -0
- package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
- package/vendor/ars/scripts/verify_passport.py +133 -0
- package/vendor/ars/scripts/verify_submission_package.py +1657 -0
- package/vendor/ars/shared/agents/compliance_agent.md +136 -0
- package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
- package/vendor/ars/shared/benchmark_report.schema.json +81 -0
- package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
- package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
- package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
- package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
- package/vendor/ars/shared/compliance_report.schema.json +187 -0
- package/vendor/ars/shared/contracts/README.md +938 -0
- package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
- package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
- package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
- package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
- package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
- package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
- package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
- package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
- package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
- package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
- package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
- package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
- package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
- package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
- package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
- package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
- package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
- package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
- package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
- package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
- package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
- package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
- package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
- package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
- package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
- package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
- package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
- package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
- package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
- package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
- package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
- package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
- package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
- package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
- package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
- package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
- package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
- package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
- package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
- package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
- package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
- package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
- package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
- package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
- package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
- package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
- package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
- package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
- package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
- package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
- package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
- package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
- package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
- package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
- package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
- package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
- package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
- package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
- package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
- package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
- package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
- package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
- package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
- package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
- package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
- package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
- package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
- package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
- package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
- package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
- package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
- package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
- package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
- package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
- package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
- package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
- package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
- package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
- package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
- package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
- package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
- package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
- package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
- package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
- package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
- package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
- package/vendor/ars/shared/contracts/writer/full.json +87 -0
- package/vendor/ars/shared/cross_model_verification.md +714 -0
- package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
- package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
- package/vendor/ars/shared/handoff_schemas.md +1209 -0
- package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
- package/vendor/ars/shared/mode_spectrum.md +57 -0
- package/vendor/ars/shared/model_tiering.md +83 -0
- package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
- package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
- package/vendor/ars/shared/raise_framework.md +129 -0
- package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
- package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
- package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
- package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
- package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
- package/vendor/ars/shared/references/firm_rules.md +90 -0
- package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
- package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
- package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
- package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
- package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
- package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
- package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
- package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
- package/vendor/ars/shared/references/word_count_conventions.md +124 -0
- package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
- package/vendor/ars/shared/review_criteria_registry.json +207 -0
- package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
- package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
- package/vendor/ars/shared/sprint_contract.schema.json +482 -0
- package/vendor/ars/shared/style_calibration_protocol.md +151 -0
- package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
- package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
- package/vendor/ars/tools/release-discipline/README.md +4 -0
- package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
- package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
- package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
- package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
- package/vendor/windows/NOTICE.md +12 -0
- package/vendor/windows/arm64/fd.exe +0 -0
- package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
- package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
- package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
- package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
- package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
- package/vendor/windows/arm64/rg.exe +0 -0
- package/vendor/windows/x64/fd.exe +0 -0
- package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
- package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
- package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
- package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
- package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
- package/vendor/windows/x64/rg.exe +0 -0
|
@@ -0,0 +1,2959 @@
|
|
|
1
|
+
"""Hermetic conformance and mutation tests for issue #660."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import unicodedata
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import yaml
|
|
15
|
+
from jsonschema import Draft202012Validator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SCRIPTS = Path(__file__).resolve().parent
|
|
19
|
+
REPO_ROOT = SCRIPTS.parent
|
|
20
|
+
FIXTURES = SCRIPTS / "fixtures/tortured_phrase_screening"
|
|
21
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
22
|
+
|
|
23
|
+
import tortured_phrase_screening as screening # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
CHECKED_AT = "2026-08-10T01:00:00Z"
|
|
27
|
+
RECORDED_AT = "2026-08-10T01:00:01Z"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _bundle() -> screening.SnapshotBundle:
|
|
31
|
+
return screening.load_snapshot(
|
|
32
|
+
FIXTURES / "snapshot.json", FIXTURES / "snapshot_manifest.json"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _state() -> screening.SnapshotState:
|
|
37
|
+
bundle = _bundle()
|
|
38
|
+
return screening.SnapshotState(
|
|
39
|
+
status="loaded",
|
|
40
|
+
reason_code="CHECK_COMPLETED",
|
|
41
|
+
bundle=bundle,
|
|
42
|
+
snapshot_sha256=bundle.snapshot_sha256,
|
|
43
|
+
manifest_sha256=bundle.manifest_sha256,
|
|
44
|
+
detail=None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _draft(name: str) -> str:
|
|
49
|
+
return (FIXTURES / name).read_text(encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _report(name: str = "own_draft.md") -> dict:
|
|
53
|
+
return screening.build_own_draft_report(
|
|
54
|
+
_draft(name),
|
|
55
|
+
artifact_id=name,
|
|
56
|
+
artifact_format="latex" if name.endswith(".tex") else "markdown",
|
|
57
|
+
state=_state(),
|
|
58
|
+
checked_at=CHECKED_AT,
|
|
59
|
+
recorded_at=RECORDED_AT,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _passport() -> dict:
|
|
64
|
+
return yaml.safe_load((FIXTURES / "corpus_input.yaml").read_text(encoding="utf-8"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _enriched(state: screening.SnapshotState | None = None) -> dict:
|
|
68
|
+
return screening.enrich_passport(
|
|
69
|
+
_passport(),
|
|
70
|
+
state=state or _state(),
|
|
71
|
+
checked_at=CHECKED_AT,
|
|
72
|
+
recorded_at=RECORDED_AT,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _seed_expectations() -> dict:
|
|
77
|
+
value, _raw = screening._strict_json_path(
|
|
78
|
+
FIXTURES / "seed_expectations.json",
|
|
79
|
+
label="synthetic seed expectations",
|
|
80
|
+
maximum=512 * 1024,
|
|
81
|
+
)
|
|
82
|
+
if not isinstance(value, dict):
|
|
83
|
+
raise AssertionError("seed expectations root must be an object")
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _expect_exact_keys(
|
|
88
|
+
value: object,
|
|
89
|
+
*,
|
|
90
|
+
required: set[str],
|
|
91
|
+
optional: set[str] | None = None,
|
|
92
|
+
label: str,
|
|
93
|
+
) -> dict:
|
|
94
|
+
if not isinstance(value, dict):
|
|
95
|
+
raise AssertionError(f"{label} must be an object")
|
|
96
|
+
optional = optional or set()
|
|
97
|
+
keys = set(value)
|
|
98
|
+
if keys != required | (keys & optional):
|
|
99
|
+
missing = required - keys
|
|
100
|
+
unknown = keys - required - optional
|
|
101
|
+
raise AssertionError(
|
|
102
|
+
f"{label} keys drifted; missing={sorted(missing)!r}, "
|
|
103
|
+
f"unknown={sorted(unknown)!r}"
|
|
104
|
+
)
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _compiled_rules() -> dict[str, screening.CompiledRule]:
|
|
109
|
+
return {rule.rule_id: rule for rule in _bundle().rules}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _actual_suppressions(
|
|
113
|
+
text: str,
|
|
114
|
+
segments: list[screening.Segment],
|
|
115
|
+
rules: dict[str, screening.CompiledRule],
|
|
116
|
+
) -> Counter[tuple[str, str]]:
|
|
117
|
+
suppressed: Counter[tuple[str, str]] = Counter()
|
|
118
|
+
for segment in segments:
|
|
119
|
+
tokens = screening.tokenize(text[segment.start : segment.end])
|
|
120
|
+
for rule in rules.values():
|
|
121
|
+
included = set(screening.evaluate_expression(rule.expression, tokens))
|
|
122
|
+
retained = set(screening.evaluate_rule(rule, tokens))
|
|
123
|
+
suppressed[(rule.rule_id, segment.kind)] += len(included - retained)
|
|
124
|
+
return +suppressed
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _has_token_boundary_negative(
|
|
128
|
+
rule: screening.CompiledRule,
|
|
129
|
+
text: str,
|
|
130
|
+
segments: list[screening.Segment],
|
|
131
|
+
) -> bool:
|
|
132
|
+
expression = rule.expression
|
|
133
|
+
if expression.get("op") != "literal":
|
|
134
|
+
return False
|
|
135
|
+
literal_tokens = screening.tokenize(expression["value"])
|
|
136
|
+
if len(literal_tokens) != 1:
|
|
137
|
+
return False
|
|
138
|
+
literal = literal_tokens[0].normalized
|
|
139
|
+
return any(
|
|
140
|
+
literal in token.normalized and literal != token.normalized
|
|
141
|
+
for segment in segments
|
|
142
|
+
for token in screening.tokenize(text[segment.start : segment.end])
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _has_same_segment_exclusion(
|
|
147
|
+
rule: screening.CompiledRule,
|
|
148
|
+
text: str,
|
|
149
|
+
segments: list[screening.Segment],
|
|
150
|
+
) -> bool:
|
|
151
|
+
if not rule.exclude_if:
|
|
152
|
+
return False
|
|
153
|
+
for segment in segments:
|
|
154
|
+
tokens = screening.tokenize(text[segment.start : segment.end])
|
|
155
|
+
included = set(screening.evaluate_expression(rule.expression, tokens))
|
|
156
|
+
if included and included - set(screening.evaluate_rule(rule, tokens)):
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _non_match_reason_holds(
|
|
162
|
+
reason: str,
|
|
163
|
+
rule: screening.CompiledRule,
|
|
164
|
+
text: str,
|
|
165
|
+
segments: list[screening.Segment],
|
|
166
|
+
) -> bool:
|
|
167
|
+
if reason == "token_boundary":
|
|
168
|
+
return _has_token_boundary_negative(rule, text, segments)
|
|
169
|
+
if reason == "exclude_if_in_same_segment":
|
|
170
|
+
return _has_same_segment_exclusion(rule, text, segments)
|
|
171
|
+
raise AssertionError(f"unsupported synthetic non-match reason {reason!r}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _normalization_case_holds(
|
|
175
|
+
case: str,
|
|
176
|
+
match: dict,
|
|
177
|
+
rule: screening.CompiledRule,
|
|
178
|
+
) -> bool:
|
|
179
|
+
expression = rule.expression
|
|
180
|
+
if expression.get("op") != "literal":
|
|
181
|
+
return False
|
|
182
|
+
raw = match["matched_text"]
|
|
183
|
+
actual_tokens = tuple(token.normalized for token in screening.tokenize(raw))
|
|
184
|
+
literal_tokens = tuple(
|
|
185
|
+
token.normalized for token in screening.tokenize(expression["value"])
|
|
186
|
+
)
|
|
187
|
+
if actual_tokens != literal_tokens:
|
|
188
|
+
return False
|
|
189
|
+
if case == "nfkc_casefold":
|
|
190
|
+
folded = unicodedata.normalize("NFKC", raw).casefold()
|
|
191
|
+
return folded == expression["value"].casefold() and raw.casefold() != folded
|
|
192
|
+
if case == "same_line_dash_separator":
|
|
193
|
+
return "\n" not in raw and any(char in screening._DASHES for char in raw)
|
|
194
|
+
if case == "soft_hyphen_join":
|
|
195
|
+
return "\u00ad" in raw and "\n" not in raw
|
|
196
|
+
if case == "line_break_hyphen_join":
|
|
197
|
+
return any(
|
|
198
|
+
char in screening._DASHES and index + 1 < len(raw) and raw[index + 1] == "\n"
|
|
199
|
+
for index, char in enumerate(raw)
|
|
200
|
+
)
|
|
201
|
+
raise AssertionError(f"unsupported normalization case {case!r}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _alternative_matches_record(
|
|
205
|
+
alternative: dict,
|
|
206
|
+
match: dict,
|
|
207
|
+
*,
|
|
208
|
+
text: str,
|
|
209
|
+
segments: dict[str, screening.Segment],
|
|
210
|
+
) -> bool:
|
|
211
|
+
segment = segments[match["segment_id"]]
|
|
212
|
+
tokens = screening.tokenize(text[segment.start : segment.end])
|
|
213
|
+
span = match["source_span"]
|
|
214
|
+
return any(
|
|
215
|
+
segment.start + witness.codepoint_start == span["codepoint_start"]
|
|
216
|
+
and segment.start + witness.codepoint_end == span["codepoint_end"]
|
|
217
|
+
for witness in screening.evaluate_expression(alternative, tokens)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _spans_overlap(left: dict, right: dict) -> bool:
|
|
222
|
+
if left["segment_id"] != right["segment_id"]:
|
|
223
|
+
return False
|
|
224
|
+
left_span = left["source_span"]
|
|
225
|
+
right_span = right["source_span"]
|
|
226
|
+
return max(left_span["codepoint_start"], right_span["codepoint_start"]) < min(
|
|
227
|
+
left_span["codepoint_end"], right_span["codepoint_end"]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _replay_draft_expectation(
|
|
232
|
+
expectation: dict,
|
|
233
|
+
rules: dict[str, screening.CompiledRule],
|
|
234
|
+
) -> set[str]:
|
|
235
|
+
expectation = _expect_exact_keys(
|
|
236
|
+
expectation,
|
|
237
|
+
required={
|
|
238
|
+
"path",
|
|
239
|
+
"expected_counts",
|
|
240
|
+
"expected_matches",
|
|
241
|
+
"expected_suppressions",
|
|
242
|
+
"expected_non_matches",
|
|
243
|
+
},
|
|
244
|
+
label="draft expectation",
|
|
245
|
+
)
|
|
246
|
+
path = expectation["path"]
|
|
247
|
+
if path not in {"own_draft.md", "own_draft.tex"}:
|
|
248
|
+
raise AssertionError(f"unsupported draft expectation path {path!r}")
|
|
249
|
+
text = _draft(path)
|
|
250
|
+
artifact_format = "latex" if path.endswith(".tex") else "markdown"
|
|
251
|
+
segments = screening.segment_document(text, artifact_format)
|
|
252
|
+
segments_by_id = {segment.segment_id: segment for segment in segments}
|
|
253
|
+
report = _report(path)
|
|
254
|
+
if report["evaluation_status"] != "UNMEASURED":
|
|
255
|
+
raise AssertionError(f"{path}: synthetic results must remain UNMEASURED")
|
|
256
|
+
|
|
257
|
+
expected_counts = _expect_exact_keys(
|
|
258
|
+
expectation["expected_counts"],
|
|
259
|
+
required={
|
|
260
|
+
"matched_rule_count",
|
|
261
|
+
"rule_match_count",
|
|
262
|
+
"unique_instance_count",
|
|
263
|
+
},
|
|
264
|
+
label=f"{path}.expected_counts",
|
|
265
|
+
)
|
|
266
|
+
for key, expected in expected_counts.items():
|
|
267
|
+
if type(expected) is not int or expected < 0:
|
|
268
|
+
raise AssertionError(f"{path}.expected_counts.{key} must be nonnegative int")
|
|
269
|
+
if report["counts"][key] != expected:
|
|
270
|
+
raise AssertionError(
|
|
271
|
+
f"{path}.{key}: expected {expected}, got {report['counts'][key]}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
expected_match_counts: Counter[tuple[str, str]] = Counter()
|
|
275
|
+
seen_match_keys: set[tuple[str, str]] = set()
|
|
276
|
+
match_groups: dict[str, list[dict]] = {}
|
|
277
|
+
group_by_match_id: dict[str, str | None] = {}
|
|
278
|
+
normalization_cases: set[str] = set()
|
|
279
|
+
for index, raw_expected in enumerate(expectation["expected_matches"]):
|
|
280
|
+
expected = _expect_exact_keys(
|
|
281
|
+
raw_expected,
|
|
282
|
+
required={"rule_id", "count", "context"},
|
|
283
|
+
optional={
|
|
284
|
+
"matched_alternative_index",
|
|
285
|
+
"overlap_group",
|
|
286
|
+
"normalization_case",
|
|
287
|
+
},
|
|
288
|
+
label=f"{path}.expected_matches[{index}]",
|
|
289
|
+
)
|
|
290
|
+
rule_id = expected["rule_id"]
|
|
291
|
+
context = expected["context"]
|
|
292
|
+
count = expected["count"]
|
|
293
|
+
if rule_id not in rules:
|
|
294
|
+
raise AssertionError(f"{path}: unknown expected rule {rule_id!r}")
|
|
295
|
+
if context not in screening.CONTEXTS:
|
|
296
|
+
raise AssertionError(f"{path}: unknown expected context {context!r}")
|
|
297
|
+
if type(count) is not int or count < 1:
|
|
298
|
+
raise AssertionError(f"{path}: expected match count must be positive int")
|
|
299
|
+
match_key = (rule_id, context)
|
|
300
|
+
if match_key in seen_match_keys:
|
|
301
|
+
raise AssertionError(f"{path}: duplicate expected match key {match_key!r}")
|
|
302
|
+
seen_match_keys.add(match_key)
|
|
303
|
+
expected_match_counts[match_key] = count
|
|
304
|
+
actual_matches = [
|
|
305
|
+
match
|
|
306
|
+
for match in report["matches"]
|
|
307
|
+
if (match["pattern_id"], match["context"]) == match_key
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
expression = rules[rule_id].expression
|
|
311
|
+
alternative_index = expected.get("matched_alternative_index")
|
|
312
|
+
if expression.get("op") == "any":
|
|
313
|
+
if type(alternative_index) is not int:
|
|
314
|
+
raise AssertionError(f"{path}: any rule {rule_id!r} needs an alternative index")
|
|
315
|
+
alternatives = expression["alternatives"]
|
|
316
|
+
if not 0 <= alternative_index < len(alternatives):
|
|
317
|
+
raise AssertionError(f"{path}: alternative index out of range")
|
|
318
|
+
for match in actual_matches:
|
|
319
|
+
matching_indexes = {
|
|
320
|
+
child_index
|
|
321
|
+
for child_index, alternative in enumerate(alternatives)
|
|
322
|
+
if _alternative_matches_record(
|
|
323
|
+
alternative,
|
|
324
|
+
match,
|
|
325
|
+
text=text,
|
|
326
|
+
segments=segments_by_id,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if matching_indexes != {alternative_index}:
|
|
330
|
+
raise AssertionError(
|
|
331
|
+
f"{path}: any alternative mismatch for {rule_id!r}: "
|
|
332
|
+
f"{sorted(matching_indexes)!r}"
|
|
333
|
+
)
|
|
334
|
+
elif alternative_index is not None:
|
|
335
|
+
raise AssertionError(f"{path}: non-any rule carries alternative index")
|
|
336
|
+
|
|
337
|
+
overlap_group = expected.get("overlap_group")
|
|
338
|
+
if overlap_group is not None and (
|
|
339
|
+
not isinstance(overlap_group, str) or not overlap_group
|
|
340
|
+
):
|
|
341
|
+
raise AssertionError(f"{path}: overlap_group must be a nonempty string")
|
|
342
|
+
for match in actual_matches:
|
|
343
|
+
group_by_match_id[match["match_id"]] = overlap_group
|
|
344
|
+
if overlap_group is not None:
|
|
345
|
+
match_groups.setdefault(overlap_group, []).append(match)
|
|
346
|
+
|
|
347
|
+
normalization_case = expected.get("normalization_case")
|
|
348
|
+
if normalization_case is not None:
|
|
349
|
+
if not isinstance(normalization_case, str):
|
|
350
|
+
raise AssertionError(f"{path}: normalization_case must be a string")
|
|
351
|
+
normalization_cases.add(normalization_case)
|
|
352
|
+
if not actual_matches or not all(
|
|
353
|
+
_normalization_case_holds(normalization_case, match, rules[rule_id])
|
|
354
|
+
for match in actual_matches
|
|
355
|
+
):
|
|
356
|
+
raise AssertionError(
|
|
357
|
+
f"{path}: normalization case {normalization_case!r} did not replay"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
actual_match_counts = Counter(
|
|
361
|
+
(match["pattern_id"], match["context"]) for match in report["matches"]
|
|
362
|
+
)
|
|
363
|
+
if actual_match_counts != expected_match_counts:
|
|
364
|
+
raise AssertionError(
|
|
365
|
+
f"{path}: expected rule/context counts do not match runtime output"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
for group, matches in match_groups.items():
|
|
369
|
+
if len(matches) < 2 or screening._unique_instance_count(matches) != 1:
|
|
370
|
+
raise AssertionError(f"{path}: overlap group {group!r} is not one component")
|
|
371
|
+
for left_index, left in enumerate(report["matches"]):
|
|
372
|
+
for right in report["matches"][left_index + 1 :]:
|
|
373
|
+
if _spans_overlap(left, right) and (
|
|
374
|
+
group_by_match_id.get(left["match_id"]) is None
|
|
375
|
+
or group_by_match_id.get(left["match_id"])
|
|
376
|
+
!= group_by_match_id.get(right["match_id"])
|
|
377
|
+
):
|
|
378
|
+
raise AssertionError(f"{path}: overlapping matches lack one shared group")
|
|
379
|
+
|
|
380
|
+
expected_suppressions: Counter[tuple[str, str]] = Counter()
|
|
381
|
+
for index, raw_expected in enumerate(expectation["expected_suppressions"]):
|
|
382
|
+
expected = _expect_exact_keys(
|
|
383
|
+
raw_expected,
|
|
384
|
+
required={"rule_id", "count", "context", "reason"},
|
|
385
|
+
label=f"{path}.expected_suppressions[{index}]",
|
|
386
|
+
)
|
|
387
|
+
rule_id = expected["rule_id"]
|
|
388
|
+
count = expected["count"]
|
|
389
|
+
context = expected["context"]
|
|
390
|
+
if rule_id not in rules or not rules[rule_id].exclude_if:
|
|
391
|
+
raise AssertionError(f"{path}: suppression names a non-excluding rule")
|
|
392
|
+
if context not in screening.CONTEXTS:
|
|
393
|
+
raise AssertionError(f"{path}: suppression context is unknown")
|
|
394
|
+
if type(count) is not int or count < 1:
|
|
395
|
+
raise AssertionError(f"{path}: suppression count must be positive int")
|
|
396
|
+
if expected["reason"] != "exclude_if_within_token_window":
|
|
397
|
+
raise AssertionError(f"{path}: unsupported suppression reason")
|
|
398
|
+
key = (rule_id, context)
|
|
399
|
+
if key in expected_suppressions:
|
|
400
|
+
raise AssertionError(f"{path}: duplicate expected suppression {key!r}")
|
|
401
|
+
expected_suppressions[key] = count
|
|
402
|
+
if _actual_suppressions(text, segments, rules) != expected_suppressions:
|
|
403
|
+
raise AssertionError(f"{path}: suppression oracle does not replay")
|
|
404
|
+
|
|
405
|
+
non_match_reasons: set[str] = set()
|
|
406
|
+
seen_non_matches: set[str] = set()
|
|
407
|
+
for index, raw_expected in enumerate(expectation["expected_non_matches"]):
|
|
408
|
+
expected = _expect_exact_keys(
|
|
409
|
+
raw_expected,
|
|
410
|
+
required={"rule_id", "reason"},
|
|
411
|
+
label=f"{path}.expected_non_matches[{index}]",
|
|
412
|
+
)
|
|
413
|
+
rule_id = expected["rule_id"]
|
|
414
|
+
reason = expected["reason"]
|
|
415
|
+
if rule_id not in rules:
|
|
416
|
+
raise AssertionError(f"{path}: non-match names unknown rule {rule_id!r}")
|
|
417
|
+
if rule_id in seen_non_matches:
|
|
418
|
+
raise AssertionError(f"{path}: duplicate non-match rule {rule_id!r}")
|
|
419
|
+
seen_non_matches.add(rule_id)
|
|
420
|
+
if any(match["pattern_id"] == rule_id for match in report["matches"]):
|
|
421
|
+
raise AssertionError(f"{path}: expected non-match {rule_id!r} matched")
|
|
422
|
+
if not _non_match_reason_holds(reason, rules[rule_id], text, segments):
|
|
423
|
+
raise AssertionError(f"{path}: non-match reason {reason!r} did not replay")
|
|
424
|
+
non_match_reasons.add(reason)
|
|
425
|
+
return normalization_cases | {f"nonmatch:{reason}" for reason in non_match_reasons}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _corpus_surface_text(entry: dict, surface: str) -> str | None:
|
|
429
|
+
return entry["title"] if surface == "title" else entry.get("abstract")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _replay_corpus_expectation(
|
|
433
|
+
expectation: dict,
|
|
434
|
+
rules: dict[str, screening.CompiledRule],
|
|
435
|
+
) -> set[str]:
|
|
436
|
+
expectation = _expect_exact_keys(
|
|
437
|
+
expectation,
|
|
438
|
+
required={"path", "records"},
|
|
439
|
+
label="corpus expectation",
|
|
440
|
+
)
|
|
441
|
+
if expectation["path"] != "corpus_input.yaml":
|
|
442
|
+
raise AssertionError("corpus expectation path drifted")
|
|
443
|
+
source_entries = {
|
|
444
|
+
entry["citation_key"]: entry for entry in _passport()["literature_corpus"]
|
|
445
|
+
}
|
|
446
|
+
output_entries = {
|
|
447
|
+
entry["citation_key"]: entry for entry in _enriched()["literature_corpus"]
|
|
448
|
+
}
|
|
449
|
+
records = expectation["records"]
|
|
450
|
+
if not isinstance(records, list):
|
|
451
|
+
raise AssertionError("corpus records must be an array")
|
|
452
|
+
citation_keys = [
|
|
453
|
+
record.get("citation_key") if isinstance(record, dict) else None
|
|
454
|
+
for record in records
|
|
455
|
+
]
|
|
456
|
+
if (
|
|
457
|
+
any(not isinstance(key, str) or not key for key in citation_keys)
|
|
458
|
+
or len(citation_keys) != len(set(citation_keys))
|
|
459
|
+
or set(citation_keys) != set(source_entries)
|
|
460
|
+
or set(citation_keys) != set(output_entries)
|
|
461
|
+
):
|
|
462
|
+
raise AssertionError("corpus expectation citation inventory drifted")
|
|
463
|
+
|
|
464
|
+
replayed_reasons: set[str] = set()
|
|
465
|
+
surface_contexts = {"title": "cited_title", "abstract": "cited_abstract"}
|
|
466
|
+
for record_index, raw_record in enumerate(records):
|
|
467
|
+
record = _expect_exact_keys(
|
|
468
|
+
raw_record,
|
|
469
|
+
required={"citation_key", "surfaces"},
|
|
470
|
+
label=f"corpus.records[{record_index}]",
|
|
471
|
+
)
|
|
472
|
+
citation_key = record["citation_key"]
|
|
473
|
+
surfaces = _expect_exact_keys(
|
|
474
|
+
record["surfaces"],
|
|
475
|
+
required={"title", "abstract"},
|
|
476
|
+
label=f"corpus.records[{citation_key}].surfaces",
|
|
477
|
+
)
|
|
478
|
+
source_entry = source_entries[citation_key]
|
|
479
|
+
output_entry = output_entries[citation_key]
|
|
480
|
+
signals = [
|
|
481
|
+
signal
|
|
482
|
+
for signal in output_entry["bibliographic_integrity_signals"]
|
|
483
|
+
if signal.get("schema_version") == screening.SIGNAL_VERSION
|
|
484
|
+
and signal.get("signal_type") == "tortured_phrase_match"
|
|
485
|
+
]
|
|
486
|
+
if len(signals) != 2:
|
|
487
|
+
raise AssertionError(
|
|
488
|
+
f"{citation_key}: expected exactly the title and abstract signals"
|
|
489
|
+
)
|
|
490
|
+
for surface, context in surface_contexts.items():
|
|
491
|
+
expected = _expect_exact_keys(
|
|
492
|
+
surfaces[surface],
|
|
493
|
+
required={"check_status", "reason", "expected_rule_ids"},
|
|
494
|
+
optional={"non_match_reasons"},
|
|
495
|
+
label=f"corpus.records[{citation_key}].{surface}",
|
|
496
|
+
)
|
|
497
|
+
matching_signals = [
|
|
498
|
+
signal
|
|
499
|
+
for signal in signals
|
|
500
|
+
if signal["tortured_phrase_context"]["surface"] == context
|
|
501
|
+
]
|
|
502
|
+
if len(matching_signals) != 1:
|
|
503
|
+
raise AssertionError(
|
|
504
|
+
f"{citation_key}.{surface}: expected exactly one current signal"
|
|
505
|
+
)
|
|
506
|
+
signal = matching_signals[0]
|
|
507
|
+
signal_context = signal["tortured_phrase_context"]
|
|
508
|
+
if signal["check_status"] != expected["check_status"]:
|
|
509
|
+
raise AssertionError(f"{citation_key}.{surface}: status drifted")
|
|
510
|
+
if signal_context["reason_code"] != expected["reason"]:
|
|
511
|
+
raise AssertionError(f"{citation_key}.{surface}: reason drifted")
|
|
512
|
+
|
|
513
|
+
expected_rule_ids = expected["expected_rule_ids"]
|
|
514
|
+
if (
|
|
515
|
+
not isinstance(expected_rule_ids, list)
|
|
516
|
+
or any(not isinstance(rule_id, str) for rule_id in expected_rule_ids)
|
|
517
|
+
or len(expected_rule_ids) != len(set(expected_rule_ids))
|
|
518
|
+
or any(rule_id not in rules for rule_id in expected_rule_ids)
|
|
519
|
+
):
|
|
520
|
+
raise AssertionError(
|
|
521
|
+
f"{citation_key}.{surface}: expected_rule_ids are invalid"
|
|
522
|
+
)
|
|
523
|
+
actual_rule_ids = {
|
|
524
|
+
match["pattern_id"] for match in signal_context["matches"]
|
|
525
|
+
}
|
|
526
|
+
if actual_rule_ids != set(expected_rule_ids):
|
|
527
|
+
raise AssertionError(f"{citation_key}.{surface}: rule ids drifted")
|
|
528
|
+
expected_finding = (
|
|
529
|
+
"unresolved"
|
|
530
|
+
if expected["check_status"] != "checked"
|
|
531
|
+
else "detected" if expected_rule_ids else "not_detected"
|
|
532
|
+
)
|
|
533
|
+
if signal["finding"] != expected_finding:
|
|
534
|
+
raise AssertionError(f"{citation_key}.{surface}: finding drifted")
|
|
535
|
+
|
|
536
|
+
raw_non_matches = expected.get("non_match_reasons", [])
|
|
537
|
+
if expected["check_status"] == "checked" and not expected_rule_ids:
|
|
538
|
+
if not raw_non_matches:
|
|
539
|
+
raise AssertionError(
|
|
540
|
+
f"{citation_key}.{surface}: zero result lacks a rule-bound reason"
|
|
541
|
+
)
|
|
542
|
+
text = _corpus_surface_text(source_entry, surface)
|
|
543
|
+
seen_non_match_rules: set[str] = set()
|
|
544
|
+
for reason_index, raw_reason in enumerate(raw_non_matches):
|
|
545
|
+
reason_record = _expect_exact_keys(
|
|
546
|
+
raw_reason,
|
|
547
|
+
required={"rule_id", "reason"},
|
|
548
|
+
label=(
|
|
549
|
+
f"corpus.records[{citation_key}].{surface}."
|
|
550
|
+
f"non_match_reasons[{reason_index}]"
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
rule_id = reason_record["rule_id"]
|
|
554
|
+
reason = reason_record["reason"]
|
|
555
|
+
if rule_id not in rules or rule_id in seen_non_match_rules:
|
|
556
|
+
raise AssertionError(
|
|
557
|
+
f"{citation_key}.{surface}: invalid non-match rule {rule_id!r}"
|
|
558
|
+
)
|
|
559
|
+
seen_non_match_rules.add(rule_id)
|
|
560
|
+
if rule_id in actual_rule_ids or text is None:
|
|
561
|
+
raise AssertionError(
|
|
562
|
+
f"{citation_key}.{surface}: non-match rule is not absent"
|
|
563
|
+
)
|
|
564
|
+
one_segment = [screening.Segment("SEG-000001", context, 0, len(text))]
|
|
565
|
+
if not _non_match_reason_holds(
|
|
566
|
+
reason,
|
|
567
|
+
rules[rule_id],
|
|
568
|
+
text,
|
|
569
|
+
one_segment,
|
|
570
|
+
):
|
|
571
|
+
raise AssertionError(
|
|
572
|
+
f"{citation_key}.{surface}: non-match reason did not replay"
|
|
573
|
+
)
|
|
574
|
+
replayed_reasons.add(reason)
|
|
575
|
+
return replayed_reasons
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _replay_seed_expectations(expectations: dict) -> None:
|
|
579
|
+
expectations = _expect_exact_keys(
|
|
580
|
+
expectations,
|
|
581
|
+
required={
|
|
582
|
+
"schema_version",
|
|
583
|
+
"snapshot_id",
|
|
584
|
+
"snapshot_sha256",
|
|
585
|
+
"label_scope",
|
|
586
|
+
"empirical_accuracy_claimed",
|
|
587
|
+
"contextual_false_positive_labels_provided",
|
|
588
|
+
"contextual_false_negative_labels_provided",
|
|
589
|
+
"inputs",
|
|
590
|
+
},
|
|
591
|
+
label="seed expectations",
|
|
592
|
+
)
|
|
593
|
+
if expectations["schema_version"] != "tortured-phrase-seed-expectations/1.0":
|
|
594
|
+
raise AssertionError("seed expectations schema_version drifted")
|
|
595
|
+
bundle = _bundle()
|
|
596
|
+
if (
|
|
597
|
+
expectations["snapshot_id"] != bundle.snapshot["snapshot_id"]
|
|
598
|
+
or expectations["snapshot_id"] != bundle.manifest["snapshot_id"]
|
|
599
|
+
or expectations["snapshot_sha256"] != bundle.snapshot_sha256
|
|
600
|
+
):
|
|
601
|
+
raise AssertionError("seed expectations snapshot identity drifted")
|
|
602
|
+
if expectations["label_scope"] != "mechanical_matcher_conformance_only":
|
|
603
|
+
raise AssertionError("seed expectations label scope drifted")
|
|
604
|
+
for flag in (
|
|
605
|
+
"empirical_accuracy_claimed",
|
|
606
|
+
"contextual_false_positive_labels_provided",
|
|
607
|
+
"contextual_false_negative_labels_provided",
|
|
608
|
+
):
|
|
609
|
+
if expectations[flag] is not False:
|
|
610
|
+
raise AssertionError(f"seed expectations {flag} must remain false")
|
|
611
|
+
|
|
612
|
+
inputs = expectations["inputs"]
|
|
613
|
+
if not isinstance(inputs, list):
|
|
614
|
+
raise AssertionError("seed expectations inputs must be an array")
|
|
615
|
+
paths = [item.get("path") if isinstance(item, dict) else None for item in inputs]
|
|
616
|
+
required_paths = {"own_draft.md", "own_draft.tex", "corpus_input.yaml"}
|
|
617
|
+
if (
|
|
618
|
+
any(not isinstance(path, str) for path in paths)
|
|
619
|
+
or len(paths) != len(set(paths))
|
|
620
|
+
or set(paths) != required_paths
|
|
621
|
+
):
|
|
622
|
+
raise AssertionError("seed expectations input inventory drifted")
|
|
623
|
+
|
|
624
|
+
rules = _compiled_rules()
|
|
625
|
+
draft_coverage: set[str] = set()
|
|
626
|
+
corpus_non_match_reasons: set[str] = set()
|
|
627
|
+
for expectation in inputs:
|
|
628
|
+
if expectation["path"] == "corpus_input.yaml":
|
|
629
|
+
corpus_non_match_reasons |= _replay_corpus_expectation(expectation, rules)
|
|
630
|
+
else:
|
|
631
|
+
draft_coverage |= _replay_draft_expectation(expectation, rules)
|
|
632
|
+
required_normalization = {
|
|
633
|
+
"nfkc_casefold",
|
|
634
|
+
"same_line_dash_separator",
|
|
635
|
+
"soft_hyphen_join",
|
|
636
|
+
"line_break_hyphen_join",
|
|
637
|
+
}
|
|
638
|
+
if not required_normalization.issubset(draft_coverage):
|
|
639
|
+
raise AssertionError("seed expectations normalization coverage drifted")
|
|
640
|
+
if "nonmatch:token_boundary" not in draft_coverage:
|
|
641
|
+
raise AssertionError("seed expectations draft token-boundary negative is missing")
|
|
642
|
+
if corpus_non_match_reasons != {
|
|
643
|
+
"token_boundary",
|
|
644
|
+
"exclude_if_in_same_segment",
|
|
645
|
+
}:
|
|
646
|
+
raise AssertionError("seed expectations corpus non-match coverage drifted")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _strict_dump(path: Path, value: dict) -> None:
|
|
650
|
+
path.write_text(
|
|
651
|
+
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
|
652
|
+
encoding="utf-8",
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _snapshot_pair(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
|
|
657
|
+
snapshot = json.loads((FIXTURES / "snapshot.json").read_text(encoding="utf-8"))
|
|
658
|
+
manifest = json.loads(
|
|
659
|
+
(FIXTURES / "snapshot_manifest.json").read_text(encoding="utf-8")
|
|
660
|
+
)
|
|
661
|
+
snapshot_path = tmp_path / "snapshot.json"
|
|
662
|
+
manifest_path = tmp_path / "manifest.json"
|
|
663
|
+
_strict_dump(snapshot_path, snapshot)
|
|
664
|
+
manifest["snapshot_sha256"] = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
|
|
665
|
+
_strict_dump(manifest_path, manifest)
|
|
666
|
+
return snapshot_path, manifest_path, snapshot, manifest
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _rewrite_pair(
|
|
670
|
+
snapshot_path: Path,
|
|
671
|
+
manifest_path: Path,
|
|
672
|
+
snapshot: dict,
|
|
673
|
+
manifest: dict,
|
|
674
|
+
) -> None:
|
|
675
|
+
_strict_dump(snapshot_path, snapshot)
|
|
676
|
+
manifest["snapshot_sha256"] = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
|
|
677
|
+
manifest["rule_count"] = len(snapshot.get("rules", []))
|
|
678
|
+
_strict_dump(manifest_path, manifest)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def test_snapshot_and_manifest_exact_hash_bind() -> None:
|
|
682
|
+
bundle = _bundle()
|
|
683
|
+
assert bundle.snapshot_sha256 == "962879909bfdd338047dc4569ff42188c4200fcd56a0a459b0e0f9169f2446c4"
|
|
684
|
+
assert len(bundle.rules) == 17
|
|
685
|
+
assert bundle.manifest["unsupported_rule_count"] == 0
|
|
686
|
+
assert bundle.manifest["rights"]["basis"] == "synthetic_fixture"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def test_seed_expectations_are_fully_replayed_as_the_mechanical_oracle() -> None:
|
|
690
|
+
_replay_seed_expectations(_seed_expectations())
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _expectation_input(expectations: dict, path: str) -> dict:
|
|
694
|
+
return next(item for item in expectations["inputs"] if item["path"] == path)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _corpus_surface(expectations: dict, citation_key: str, surface: str) -> dict:
|
|
698
|
+
corpus = _expectation_input(expectations, "corpus_input.yaml")
|
|
699
|
+
record = next(
|
|
700
|
+
item for item in corpus["records"] if item["citation_key"] == citation_key
|
|
701
|
+
)
|
|
702
|
+
return record["surfaces"][surface]
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _mutate_seed_expectations(expectations: dict, mutation: str) -> None:
|
|
706
|
+
markdown = _expectation_input(expectations, "own_draft.md")
|
|
707
|
+
if mutation == "wrong_count":
|
|
708
|
+
markdown["expected_counts"]["rule_match_count"] += 1
|
|
709
|
+
elif mutation == "wrong_rule":
|
|
710
|
+
markdown["expected_matches"][0]["rule_id"] = "syn_corpus_abstract"
|
|
711
|
+
elif mutation == "wrong_context":
|
|
712
|
+
markdown["expected_matches"][0]["context"] = "quote"
|
|
713
|
+
elif mutation == "wrong_alternative":
|
|
714
|
+
expected = next(
|
|
715
|
+
item
|
|
716
|
+
for item in markdown["expected_matches"]
|
|
717
|
+
if "matched_alternative_index" in item
|
|
718
|
+
)
|
|
719
|
+
expected["matched_alternative_index"] += 1
|
|
720
|
+
elif mutation == "wrong_overlap":
|
|
721
|
+
expected = next(
|
|
722
|
+
item for item in markdown["expected_matches"] if "overlap_group" in item
|
|
723
|
+
)
|
|
724
|
+
expected["overlap_group"] += "-mutated"
|
|
725
|
+
elif mutation == "wrong_normalization":
|
|
726
|
+
expected = next(
|
|
727
|
+
item
|
|
728
|
+
for item in markdown["expected_matches"]
|
|
729
|
+
if item.get("normalization_case") == "nfkc_casefold"
|
|
730
|
+
)
|
|
731
|
+
expected["normalization_case"] = "same_line_dash_separator"
|
|
732
|
+
elif mutation == "wrong_suppression":
|
|
733
|
+
markdown["expected_suppressions"][0]["count"] += 1
|
|
734
|
+
elif mutation == "wrong_nonmatch":
|
|
735
|
+
markdown["expected_non_matches"][0]["reason"] = "exclude_if_in_same_segment"
|
|
736
|
+
elif mutation == "wrong_corpus_status":
|
|
737
|
+
_corpus_surface(expectations, "fixture_complete_2026", "title")[
|
|
738
|
+
"check_status"
|
|
739
|
+
] = "not_checked"
|
|
740
|
+
elif mutation == "wrong_corpus_reason":
|
|
741
|
+
_corpus_surface(expectations, "fixture_missing_2026", "abstract")[
|
|
742
|
+
"reason"
|
|
743
|
+
] = "ABSTRACT_EMPTY"
|
|
744
|
+
elif mutation == "wrong_corpus_rule_id":
|
|
745
|
+
_corpus_surface(expectations, "fixture_complete_2026", "title")[
|
|
746
|
+
"expected_rule_ids"
|
|
747
|
+
] = ["syn_corpus_abstract"]
|
|
748
|
+
elif mutation == "wrong_corpus_nonmatch":
|
|
749
|
+
_corpus_surface(expectations, "fixture_negative_2026", "title")[
|
|
750
|
+
"non_match_reasons"
|
|
751
|
+
][0]["rule_id"] = "syn_literal_author"
|
|
752
|
+
elif mutation == "wrong_corpus_nonmatch_reason":
|
|
753
|
+
_corpus_surface(expectations, "fixture_negative_2026", "title")[
|
|
754
|
+
"non_match_reasons"
|
|
755
|
+
][0]["reason"] = "exclude_if_in_same_segment"
|
|
756
|
+
elif mutation == "wrong_snapshot_identity":
|
|
757
|
+
expectations["snapshot_sha256"] = "0" * 64
|
|
758
|
+
elif mutation == "wrong_claim_flag":
|
|
759
|
+
expectations["empirical_accuracy_claimed"] = True
|
|
760
|
+
elif mutation == "unknown_root_key":
|
|
761
|
+
expectations["unexpected"] = True
|
|
762
|
+
elif mutation == "missing_root_key":
|
|
763
|
+
expectations.pop("schema_version")
|
|
764
|
+
elif mutation == "unknown_input":
|
|
765
|
+
expectations["inputs"].append({"path": "unknown.txt"})
|
|
766
|
+
elif mutation == "missing_input":
|
|
767
|
+
expectations["inputs"].pop()
|
|
768
|
+
elif mutation == "duplicate_input":
|
|
769
|
+
expectations["inputs"].append(copy.deepcopy(markdown))
|
|
770
|
+
else: # pragma: no cover - test parameter list is closed below
|
|
771
|
+
raise AssertionError(f"unknown test mutation {mutation!r}")
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@pytest.mark.parametrize(
|
|
775
|
+
"mutation",
|
|
776
|
+
[
|
|
777
|
+
"wrong_count",
|
|
778
|
+
"wrong_rule",
|
|
779
|
+
"wrong_context",
|
|
780
|
+
"wrong_alternative",
|
|
781
|
+
"wrong_overlap",
|
|
782
|
+
"wrong_normalization",
|
|
783
|
+
"wrong_suppression",
|
|
784
|
+
"wrong_nonmatch",
|
|
785
|
+
"wrong_corpus_status",
|
|
786
|
+
"wrong_corpus_reason",
|
|
787
|
+
"wrong_corpus_rule_id",
|
|
788
|
+
"wrong_corpus_nonmatch",
|
|
789
|
+
"wrong_corpus_nonmatch_reason",
|
|
790
|
+
"wrong_snapshot_identity",
|
|
791
|
+
"wrong_claim_flag",
|
|
792
|
+
"unknown_root_key",
|
|
793
|
+
"missing_root_key",
|
|
794
|
+
"unknown_input",
|
|
795
|
+
"missing_input",
|
|
796
|
+
"duplicate_input",
|
|
797
|
+
],
|
|
798
|
+
)
|
|
799
|
+
def test_seed_expectation_mutations_fail_closed(mutation: str) -> None:
|
|
800
|
+
expectations = copy.deepcopy(_seed_expectations())
|
|
801
|
+
_mutate_seed_expectations(expectations, mutation)
|
|
802
|
+
with pytest.raises(AssertionError):
|
|
803
|
+
_replay_seed_expectations(expectations)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def test_context_detection_is_never_suppressed() -> None:
|
|
807
|
+
report = _report()
|
|
808
|
+
assert report["check_status"] == "checked"
|
|
809
|
+
assert report["finding"] == "detected"
|
|
810
|
+
assert report["counts"]["unknown_segments"] == 0
|
|
811
|
+
by_rule: dict[str, list[dict]] = {}
|
|
812
|
+
for match in report["matches"]:
|
|
813
|
+
by_rule.setdefault(match["pattern_id"], []).append(match)
|
|
814
|
+
assert by_rule["syn_context_quote"][0]["context"] == "quote"
|
|
815
|
+
assert {item["context"] for item in by_rule["syn_context_code"]} == {
|
|
816
|
+
"code_or_verbatim"
|
|
817
|
+
}
|
|
818
|
+
assert by_rule["syn_context_reference"][0]["context"] == "reference_entry"
|
|
819
|
+
assert all(
|
|
820
|
+
item["disposition"] == "preserve_verbatim_review_context"
|
|
821
|
+
for rule_id in ("syn_context_quote", "syn_context_code", "syn_context_reference")
|
|
822
|
+
for item in by_rule[rule_id]
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def test_exclude_if_is_segment_scoped() -> None:
|
|
827
|
+
matches = [
|
|
828
|
+
item for item in _report()["matches"] if item["pattern_id"] == "syn_exclude_segment"
|
|
829
|
+
]
|
|
830
|
+
assert len(matches) == 1
|
|
831
|
+
assert matches[0]["context"] == "quote"
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@pytest.mark.parametrize(
|
|
835
|
+
("text", "artifact_format"),
|
|
836
|
+
[
|
|
837
|
+
("moonlit pickle\n\nrecipe glossary", "markdown"),
|
|
838
|
+
(
|
|
839
|
+
"# References\nmoonlit pickle\nrecipe glossary\n",
|
|
840
|
+
"markdown",
|
|
841
|
+
),
|
|
842
|
+
(
|
|
843
|
+
"\\begin{thebibliography}{9}\n"
|
|
844
|
+
"\\bibitem{a} moonlit pickle\n"
|
|
845
|
+
"\\bibitem{b} recipe glossary\n"
|
|
846
|
+
"\\end{thebibliography}\n",
|
|
847
|
+
"latex",
|
|
848
|
+
),
|
|
849
|
+
],
|
|
850
|
+
)
|
|
851
|
+
def test_exclude_if_never_crosses_paragraph_or_reference_entry(
|
|
852
|
+
text: str, artifact_format: str
|
|
853
|
+
) -> None:
|
|
854
|
+
rule = _compiled_rules()["syn_exclude_segment"]
|
|
855
|
+
segments = screening.segment_document(text, artifact_format)
|
|
856
|
+
retained = [
|
|
857
|
+
witness
|
|
858
|
+
for segment in segments
|
|
859
|
+
for witness in screening.evaluate_rule(
|
|
860
|
+
rule, screening.tokenize(text[segment.start : segment.end])
|
|
861
|
+
)
|
|
862
|
+
]
|
|
863
|
+
assert len(retained) == 1
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def test_overlaps_preserve_rule_hits_and_cluster_unique_instances() -> None:
|
|
867
|
+
matches = [
|
|
868
|
+
item
|
|
869
|
+
for item in _report()["matches"]
|
|
870
|
+
if item["pattern_id"] in {"syn_overlap_long", "syn_overlap_short"}
|
|
871
|
+
]
|
|
872
|
+
assert len(matches) == 2
|
|
873
|
+
assert screening._unique_instance_count(matches) == 1
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@pytest.mark.parametrize(
|
|
877
|
+
"rule_id",
|
|
878
|
+
["syn_nfkc_casefold", "syn_dash_same_line", "syn_soft_hyphen", "syn_hyphen_line_break"],
|
|
879
|
+
)
|
|
880
|
+
def test_frozen_normalization_cases_match(rule_id: str) -> None:
|
|
881
|
+
assert any(item["pattern_id"] == rule_id for item in _report()["matches"])
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
@pytest.mark.parametrize(
|
|
885
|
+
("raw", "expected", "expected_start"),
|
|
886
|
+
[
|
|
887
|
+
("a-\nb", ["ab"], 0),
|
|
888
|
+
("silver- \nleaf", ["silver", "leaf"], 0),
|
|
889
|
+
("silver—\nleaf", ["silver", "leaf"], 0),
|
|
890
|
+
("1-\na", ["1", "a"], 0),
|
|
891
|
+
("a-\n2", ["a", "2"], 0),
|
|
892
|
+
("a1-\nb", ["a1", "b"], 0),
|
|
893
|
+
("a\u0301-\nb", ["á", "b"], 0),
|
|
894
|
+
("\u0301alpha", ["alpha"], 1),
|
|
895
|
+
],
|
|
896
|
+
)
|
|
897
|
+
def test_only_exact_ascii_immediate_line_hyphen_joins_and_marks_cannot_start(
|
|
898
|
+
raw: str, expected: list[str], expected_start: int
|
|
899
|
+
) -> None:
|
|
900
|
+
tokens = screening.tokenize(raw)
|
|
901
|
+
assert [token.normalized for token in tokens] == expected
|
|
902
|
+
assert tokens[0].start == expected_start
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def test_token_boundary_prevents_substring_match() -> None:
|
|
906
|
+
assert all(item["pattern_id"] != "syn_boundary_orb" for item in _report()["matches"])
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def test_repeated_literal_hits_fail_closed_at_the_witness_cap() -> None:
|
|
910
|
+
tokens = screening.tokenize(" ".join(["orb"] * (screening.MAX_NODE_WITNESSES + 1)))
|
|
911
|
+
with pytest.raises(screening.MatchLimitError, match="literal produced more"):
|
|
912
|
+
screening.evaluate_expression({"op": "literal", "value": "orb"}, tokens)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def test_witness_reducer_deduplicates_only_exact_spans() -> None:
|
|
916
|
+
inner = screening.Witness(2, 3, 2, 3)
|
|
917
|
+
same = screening.Witness(2, 3, 2, 3)
|
|
918
|
+
outer = screening.Witness(1, 4, 1, 4)
|
|
919
|
+
separate = screening.Witness(5, 6, 5, 6)
|
|
920
|
+
assert screening._minimal_witnesses([outer, inner, same, separate]) == [
|
|
921
|
+
outer,
|
|
922
|
+
inner,
|
|
923
|
+
separate,
|
|
924
|
+
]
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def test_any_and_all_preserve_distinct_nested_witnesses() -> None:
|
|
928
|
+
tokens = screening.tokenize("amber reed reed")
|
|
929
|
+
any_values = screening.evaluate_expression(
|
|
930
|
+
{
|
|
931
|
+
"op": "any",
|
|
932
|
+
"alternatives": [
|
|
933
|
+
{"op": "literal", "value": "amber"},
|
|
934
|
+
{"op": "literal", "value": "amber reed"},
|
|
935
|
+
],
|
|
936
|
+
},
|
|
937
|
+
tokens,
|
|
938
|
+
)
|
|
939
|
+
assert [(item.token_start, item.token_end) for item in any_values] == [
|
|
940
|
+
(0, 1),
|
|
941
|
+
(0, 2),
|
|
942
|
+
]
|
|
943
|
+
all_values = screening.evaluate_expression(
|
|
944
|
+
{
|
|
945
|
+
"op": "all",
|
|
946
|
+
"terms": [
|
|
947
|
+
{"op": "literal", "value": "amber"},
|
|
948
|
+
{"op": "literal", "value": "reed"},
|
|
949
|
+
],
|
|
950
|
+
"max_span_tokens": 3,
|
|
951
|
+
},
|
|
952
|
+
tokens,
|
|
953
|
+
)
|
|
954
|
+
assert [(item.token_start, item.token_end) for item in all_values] == [
|
|
955
|
+
(0, 2),
|
|
956
|
+
(0, 3),
|
|
957
|
+
]
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def test_full_scan_counts_nested_same_rule_witnesses_as_one_overlap_instance() -> None:
|
|
961
|
+
expression = {
|
|
962
|
+
"op": "any",
|
|
963
|
+
"alternatives": [
|
|
964
|
+
{"op": "literal", "value": "amber"},
|
|
965
|
+
{"op": "literal", "value": "amber reed"},
|
|
966
|
+
],
|
|
967
|
+
}
|
|
968
|
+
base = _bundle()
|
|
969
|
+
rule = screening.CompiledRule(
|
|
970
|
+
rule_id="nested_union",
|
|
971
|
+
expression=expression,
|
|
972
|
+
exclude_if=(),
|
|
973
|
+
rule_sha256=screening._sha256_text(screening._canonical_json(expression)),
|
|
974
|
+
semantic_key=screening._canonical_json(expression),
|
|
975
|
+
)
|
|
976
|
+
bundle = screening.SnapshotBundle(
|
|
977
|
+
snapshot=base.snapshot,
|
|
978
|
+
manifest=base.manifest,
|
|
979
|
+
snapshot_sha256=base.snapshot_sha256,
|
|
980
|
+
manifest_sha256=base.manifest_sha256,
|
|
981
|
+
rules=(rule,),
|
|
982
|
+
unicode_data_version=base.unicode_data_version,
|
|
983
|
+
)
|
|
984
|
+
text = "amber reed"
|
|
985
|
+
matches, counts = screening.scan_segments(
|
|
986
|
+
text,
|
|
987
|
+
[screening.Segment("SEG-000001", "author_prose", 0, len(text))],
|
|
988
|
+
bundle,
|
|
989
|
+
artifact_sha256=hashlib.sha256(text.encode()).hexdigest(),
|
|
990
|
+
surface="own_draft",
|
|
991
|
+
)
|
|
992
|
+
assert len(matches) == 2
|
|
993
|
+
assert counts["rule_match_count"] == 2
|
|
994
|
+
assert counts["matched_rule_count"] == 1
|
|
995
|
+
assert counts["unique_instance_count"] == 1
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def test_utf8_and_codepoint_spans_replay_multibyte_text() -> None:
|
|
999
|
+
report = screening.build_own_draft_report(
|
|
1000
|
+
"前文 luminous turnip 後文",
|
|
1001
|
+
artifact_id="unicode.md",
|
|
1002
|
+
artifact_format="markdown",
|
|
1003
|
+
state=_state(),
|
|
1004
|
+
checked_at=CHECKED_AT,
|
|
1005
|
+
recorded_at=RECORDED_AT,
|
|
1006
|
+
)
|
|
1007
|
+
match = next(item for item in report["matches"] if item["pattern_id"] == "syn_literal_author")
|
|
1008
|
+
span = match["source_span"]
|
|
1009
|
+
text = "前文 luminous turnip 後文"
|
|
1010
|
+
assert text[span["codepoint_start"] : span["codepoint_end"]] == match["matched_text"]
|
|
1011
|
+
raw = text.encode("utf-8")
|
|
1012
|
+
assert raw[span["utf8_start"] : span["utf8_end"]].decode() == match["matched_text"]
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@pytest.mark.parametrize("name", ["own_draft.md", "own_draft.tex"])
|
|
1016
|
+
def test_segment_partition_covers_source_exactly(name: str) -> None:
|
|
1017
|
+
text = _draft(name)
|
|
1018
|
+
segments = screening.segment_document(text, "latex" if name.endswith(".tex") else "markdown")
|
|
1019
|
+
assert segments[0].start == 0
|
|
1020
|
+
assert segments[-1].end == len(text)
|
|
1021
|
+
assert all(left.end == right.start for left, right in zip(segments, segments[1:]))
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def test_reference_heading_inside_fence_does_not_reclassify_following_prose() -> None:
|
|
1025
|
+
text = "```text\n# References\n```\nFollowing luminous turnip."
|
|
1026
|
+
segments = screening.segment_document(text, "markdown")
|
|
1027
|
+
following = next(item for item in segments if item.start <= text.index("Following") < item.end)
|
|
1028
|
+
assert following.kind == "author_prose"
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def test_reference_heading_inside_html_comment_does_not_reclassify_prose() -> None:
|
|
1032
|
+
text = "<!--\n# References\n-->\nordinary luminous turnip\n"
|
|
1033
|
+
segments = screening.segment_document(text, "markdown")
|
|
1034
|
+
following = next(
|
|
1035
|
+
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
1036
|
+
)
|
|
1037
|
+
assert following.kind == "author_prose"
|
|
1038
|
+
report = screening.build_own_draft_report(
|
|
1039
|
+
text,
|
|
1040
|
+
artifact_id="comment-heading.md",
|
|
1041
|
+
artifact_format="markdown",
|
|
1042
|
+
state=_state(),
|
|
1043
|
+
checked_at=CHECKED_AT,
|
|
1044
|
+
recorded_at=RECORDED_AT,
|
|
1045
|
+
)
|
|
1046
|
+
prose_match = next(
|
|
1047
|
+
item for item in report["matches"] if item["matched_text"] == "luminous turnip"
|
|
1048
|
+
)
|
|
1049
|
+
assert prose_match["context"] == "author_prose"
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
@pytest.mark.parametrize("marker", ["```python", "~~~text"])
|
|
1053
|
+
def test_fence_opener_inside_html_comment_cannot_leak_unknown_context(
|
|
1054
|
+
marker: str,
|
|
1055
|
+
) -> None:
|
|
1056
|
+
text = f"<!--\n{marker}\n-->\nordinary luminous turnip\n"
|
|
1057
|
+
segments = screening.segment_document(text, "markdown")
|
|
1058
|
+
following = next(
|
|
1059
|
+
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
1060
|
+
)
|
|
1061
|
+
assert following.kind == "author_prose"
|
|
1062
|
+
assert all(item.kind != "unknown" for item in segments)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def test_latex_environment_opener_inside_comment_cannot_leak_context() -> None:
|
|
1066
|
+
text = (
|
|
1067
|
+
"% \\begin{thebibliography}{9}\n"
|
|
1068
|
+
"ordinary luminous turnip\n"
|
|
1069
|
+
"\\end{thebibliography}\n"
|
|
1070
|
+
)
|
|
1071
|
+
segments = screening.segment_document(text, "latex")
|
|
1072
|
+
following = next(
|
|
1073
|
+
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
1074
|
+
)
|
|
1075
|
+
assert following.kind == "author_prose"
|
|
1076
|
+
report = screening.build_own_draft_report(
|
|
1077
|
+
text,
|
|
1078
|
+
artifact_id="comment-environment.tex",
|
|
1079
|
+
artifact_format="latex",
|
|
1080
|
+
state=_state(),
|
|
1081
|
+
checked_at=CHECKED_AT,
|
|
1082
|
+
recorded_at=RECORDED_AT,
|
|
1083
|
+
)
|
|
1084
|
+
prose_match = next(
|
|
1085
|
+
item for item in report["matches"] if item["matched_text"] == "luminous turnip"
|
|
1086
|
+
)
|
|
1087
|
+
assert prose_match["context"] == "author_prose"
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@pytest.mark.parametrize(
|
|
1091
|
+
("environment", "expected_kind"),
|
|
1092
|
+
[
|
|
1093
|
+
("thebibliography", "reference_entry"),
|
|
1094
|
+
("quote", "quote"),
|
|
1095
|
+
("verbatim", "code_or_verbatim"),
|
|
1096
|
+
],
|
|
1097
|
+
)
|
|
1098
|
+
def test_latex_environment_escape_parity(
|
|
1099
|
+
environment: str, expected_kind: str
|
|
1100
|
+
) -> None:
|
|
1101
|
+
even = (
|
|
1102
|
+
"\\" * 2
|
|
1103
|
+
+ f"begin{{{environment}}}\nordinary\n"
|
|
1104
|
+
+ "\\" * 2
|
|
1105
|
+
+ f"end{{{environment}}}\n"
|
|
1106
|
+
)
|
|
1107
|
+
assert all(
|
|
1108
|
+
item.kind == "author_prose"
|
|
1109
|
+
for item in screening.segment_document(even, "latex")
|
|
1110
|
+
)
|
|
1111
|
+
odd = (
|
|
1112
|
+
"\\" * 3
|
|
1113
|
+
+ f"begin{{{environment}}}\nordinary\n"
|
|
1114
|
+
+ "\\" * 3
|
|
1115
|
+
+ f"end{{{environment}}}\n"
|
|
1116
|
+
)
|
|
1117
|
+
assert any(
|
|
1118
|
+
item.kind == expected_kind
|
|
1119
|
+
for item in screening.segment_document(odd, "latex")
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def test_latex_verb_escape_parity_and_opaque_opener_precedence() -> None:
|
|
1124
|
+
even = r"\\verb|ordinary luminous turnip|"
|
|
1125
|
+
assert all(
|
|
1126
|
+
item.kind == "author_prose"
|
|
1127
|
+
for item in screening.segment_document(even, "latex")
|
|
1128
|
+
)
|
|
1129
|
+
active = r"\verb|\begin{verbatim}| ordinary luminous turnip"
|
|
1130
|
+
segments = screening.segment_document(active, "latex")
|
|
1131
|
+
following = next(
|
|
1132
|
+
item for item in segments if item.start <= active.index("ordinary") < item.end
|
|
1133
|
+
)
|
|
1134
|
+
assert following.kind == "author_prose"
|
|
1135
|
+
assert all(item.kind != "unknown" for item in segments)
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def test_source_order_opaque_lexer_does_not_let_inner_opener_consume_later_close() -> None:
|
|
1139
|
+
text = (
|
|
1140
|
+
r"\verb|\begin{verbatim}| "
|
|
1141
|
+
r"\begin{verbatim} recipe glossary \end{verbatim} moonlit pickle"
|
|
1142
|
+
)
|
|
1143
|
+
segments = screening.segment_document(text, "latex")
|
|
1144
|
+
real_environment = text.index(r"\begin{verbatim}", len(r"\verb|"))
|
|
1145
|
+
later_prose = text.index("moonlit pickle")
|
|
1146
|
+
assert next(
|
|
1147
|
+
item for item in segments if item.start <= real_environment < item.end
|
|
1148
|
+
).kind == "code_or_verbatim"
|
|
1149
|
+
assert next(
|
|
1150
|
+
item for item in segments if item.start <= later_prose < item.end
|
|
1151
|
+
).kind == "author_prose"
|
|
1152
|
+
report = screening.build_own_draft_report(
|
|
1153
|
+
text,
|
|
1154
|
+
artifact_id="source-order.tex",
|
|
1155
|
+
artifact_format="latex",
|
|
1156
|
+
state=_state(),
|
|
1157
|
+
checked_at=CHECKED_AT,
|
|
1158
|
+
recorded_at=RECORDED_AT,
|
|
1159
|
+
)
|
|
1160
|
+
assert any(
|
|
1161
|
+
item["pattern_id"] == "syn_exclude_segment"
|
|
1162
|
+
and item["matched_text"] == "moonlit pickle"
|
|
1163
|
+
for item in report["matches"]
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def test_source_order_markdown_comment_does_not_consume_later_inline_code() -> None:
|
|
1168
|
+
text = "<!-- ` --> `luminous turnip`"
|
|
1169
|
+
segments = screening.segment_document(text, "markdown")
|
|
1170
|
+
later = text.index("`luminous")
|
|
1171
|
+
assert next(item for item in segments if item.start <= later < item.end).kind == (
|
|
1172
|
+
"code_or_verbatim"
|
|
1173
|
+
)
|
|
1174
|
+
assert all(item.kind != "unknown" for item in segments)
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def test_blockquote_marker_inside_comment_cannot_capture_later_prose() -> None:
|
|
1178
|
+
text = '<!--\n> --> "recipe glossary" moonlit pickle\n'
|
|
1179
|
+
segments = screening.segment_document(text, "markdown")
|
|
1180
|
+
target = text.index("moonlit pickle")
|
|
1181
|
+
assert next(item for item in segments if item.start <= target < item.end).kind == (
|
|
1182
|
+
"author_prose"
|
|
1183
|
+
)
|
|
1184
|
+
report = screening.build_own_draft_report(
|
|
1185
|
+
text,
|
|
1186
|
+
artifact_id="comment-blockquote.md",
|
|
1187
|
+
artifact_format="markdown",
|
|
1188
|
+
state=_state(),
|
|
1189
|
+
checked_at=CHECKED_AT,
|
|
1190
|
+
recorded_at=RECORDED_AT,
|
|
1191
|
+
)
|
|
1192
|
+
assert any(
|
|
1193
|
+
item["pattern_id"] == "syn_exclude_segment"
|
|
1194
|
+
and item["matched_text"] == "moonlit pickle"
|
|
1195
|
+
for item in report["matches"]
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def test_context_openers_inside_opaque_bytes_do_not_consume_later_context() -> None:
|
|
1200
|
+
text = '`"` "recipe glossary" moonlit pickle '
|
|
1201
|
+
text += '`[` [Visible title](https://doi.org/10.1/example)'
|
|
1202
|
+
segments = screening.segment_document(text, "markdown")
|
|
1203
|
+
glossary = text.index('"recipe glossary"')
|
|
1204
|
+
title = text.index("Visible title")
|
|
1205
|
+
assert next(item for item in segments if item.start <= glossary < item.end).kind == (
|
|
1206
|
+
"quote"
|
|
1207
|
+
)
|
|
1208
|
+
assert next(item for item in segments if item.start <= title < item.end).kind == (
|
|
1209
|
+
"cited_title"
|
|
1210
|
+
)
|
|
1211
|
+
report = screening.build_own_draft_report(
|
|
1212
|
+
text,
|
|
1213
|
+
artifact_id="opaque-context.md",
|
|
1214
|
+
artifact_format="markdown",
|
|
1215
|
+
state=_state(),
|
|
1216
|
+
checked_at=CHECKED_AT,
|
|
1217
|
+
recorded_at=RECORDED_AT,
|
|
1218
|
+
)
|
|
1219
|
+
assert any(
|
|
1220
|
+
item["pattern_id"] == "syn_exclude_segment"
|
|
1221
|
+
and item["matched_text"] == "moonlit pickle"
|
|
1222
|
+
for item in report["matches"]
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
@pytest.mark.parametrize(
|
|
1227
|
+
"text",
|
|
1228
|
+
[
|
|
1229
|
+
r"\verbose ordinary luminous turnip",
|
|
1230
|
+
r"\verbatim ordinary luminous turnip",
|
|
1231
|
+
],
|
|
1232
|
+
)
|
|
1233
|
+
def test_latex_verb_control_word_prefixes_remain_prose(text: str) -> None:
|
|
1234
|
+
assert all(
|
|
1235
|
+
item.kind == "author_prose"
|
|
1236
|
+
for item in screening.segment_document(text, "latex")
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def test_latex_starred_verb_and_nested_quote_are_recognized() -> None:
|
|
1241
|
+
starred = r"\verb*|ordinary luminous turnip|"
|
|
1242
|
+
assert any(
|
|
1243
|
+
item.kind == "code_or_verbatim"
|
|
1244
|
+
and starred[item.start : item.end] == starred
|
|
1245
|
+
for item in screening.segment_document(starred, "latex")
|
|
1246
|
+
)
|
|
1247
|
+
nested = (
|
|
1248
|
+
r"\begin{quote} outer \begin{quote} inner \end{quote} "
|
|
1249
|
+
r"tail \end{quote}"
|
|
1250
|
+
)
|
|
1251
|
+
tail = nested.index("tail")
|
|
1252
|
+
assert next(
|
|
1253
|
+
item
|
|
1254
|
+
for item in screening.segment_document(nested, "latex")
|
|
1255
|
+
if item.start <= tail < item.end
|
|
1256
|
+
).kind == "quote"
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
@pytest.mark.parametrize("text", [r"\verb ", "\\verb\n", r"\verb* "])
|
|
1260
|
+
def test_malformed_bare_latex_verb_is_unknown(text: str) -> None:
|
|
1261
|
+
assert any(
|
|
1262
|
+
item.kind == "unknown"
|
|
1263
|
+
for item in screening.segment_document(text, "latex")
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def test_starred_latex_verb_cannot_backtrack_into_unstarred_delimiter() -> None:
|
|
1268
|
+
text = r"\verb* foo * ordinary luminous turnip"
|
|
1269
|
+
segments = screening.segment_document(text, "latex")
|
|
1270
|
+
assert len(segments) == 1
|
|
1271
|
+
assert segments[0].kind == "unknown"
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def test_latex_accent_macro_quotes_remain_author_prose() -> None:
|
|
1275
|
+
text = r'\"o ordinary luminous turnip \"u'
|
|
1276
|
+
assert all(
|
|
1277
|
+
item.kind == "author_prose"
|
|
1278
|
+
for item in screening.segment_document(text, "latex")
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
@pytest.mark.parametrize(
|
|
1283
|
+
"target",
|
|
1284
|
+
[
|
|
1285
|
+
"HTTPS://DOI.ORG/10.1/example",
|
|
1286
|
+
"HTTP://DX.DOI.ORG/10.1/example",
|
|
1287
|
+
"DOI:10.1/example",
|
|
1288
|
+
],
|
|
1289
|
+
)
|
|
1290
|
+
def test_doi_title_prefix_is_ascii_case_insensitive(target: str) -> None:
|
|
1291
|
+
text = f"[Cited Turnip]({target})"
|
|
1292
|
+
title = text.index("Cited Turnip")
|
|
1293
|
+
assert next(
|
|
1294
|
+
item
|
|
1295
|
+
for item in screening.segment_document(text, "markdown")
|
|
1296
|
+
if item.start <= title < item.end
|
|
1297
|
+
).kind == "cited_title"
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def test_opaque_opener_cap_precedes_unclosed_verb_pairing(
|
|
1301
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1302
|
+
) -> None:
|
|
1303
|
+
monkeypatch.setattr(screening, "MAX_PARSE_INTERVALS", 3)
|
|
1304
|
+
with pytest.raises(screening.MatchLimitError, match="opaque parse candidates"):
|
|
1305
|
+
screening.segment_document(r"\verb!" * 4, "latex")
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def test_unclosed_context_scanners_advance_monotonically() -> None:
|
|
1309
|
+
text = "[" * 20_000 + "\n" + "“" * 10_000
|
|
1310
|
+
segments = screening.segment_document(text, "markdown")
|
|
1311
|
+
assert len(segments) == 1
|
|
1312
|
+
assert segments[0].kind == "author_prose"
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def test_escaped_latex_environment_closer_does_not_close_early() -> None:
|
|
1316
|
+
text = (
|
|
1317
|
+
"\\begin{thebibliography}{9}\n"
|
|
1318
|
+
"\\\\end{thebibliography}\n"
|
|
1319
|
+
"ordinary luminous turnip\n"
|
|
1320
|
+
"\\end{thebibliography}\n"
|
|
1321
|
+
)
|
|
1322
|
+
following = next(
|
|
1323
|
+
item
|
|
1324
|
+
for item in screening.segment_document(text, "latex")
|
|
1325
|
+
if item.start <= text.index("ordinary") < item.end
|
|
1326
|
+
)
|
|
1327
|
+
assert following.kind == "reference_entry"
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
@pytest.mark.parametrize(
|
|
1331
|
+
"text",
|
|
1332
|
+
[
|
|
1333
|
+
r"\(\begin{quote}\) ordinary luminous turnip",
|
|
1334
|
+
r"$\begin{thebibliography}$ ordinary luminous turnip",
|
|
1335
|
+
r"\[\begin{verbatim}\] ordinary luminous turnip",
|
|
1336
|
+
],
|
|
1337
|
+
)
|
|
1338
|
+
def test_latex_environment_openers_inside_math_are_opaque(text: str) -> None:
|
|
1339
|
+
segments = screening.segment_document(text, "latex")
|
|
1340
|
+
following = next(
|
|
1341
|
+
item for item in segments if item.start <= text.index("ordinary") < item.end
|
|
1342
|
+
)
|
|
1343
|
+
assert following.kind == "author_prose"
|
|
1344
|
+
assert all(item.kind != "unknown" for item in segments)
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
@pytest.mark.parametrize("artifact_format", ["markdown", "latex"])
|
|
1348
|
+
def test_odd_backslash_escaped_dollar_is_literal_prose(
|
|
1349
|
+
artifact_format: str,
|
|
1350
|
+
) -> None:
|
|
1351
|
+
text = r"Cost is \$5."
|
|
1352
|
+
segments = screening.segment_document(text, artifact_format)
|
|
1353
|
+
assert all(segment.kind == "author_prose" for segment in segments)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def test_markdown_backtick_escape_parity_is_respected() -> None:
|
|
1357
|
+
escaped = r"Literal \` tick."
|
|
1358
|
+
assert all(
|
|
1359
|
+
segment.kind == "author_prose"
|
|
1360
|
+
for segment in screening.segment_document(escaped, "markdown")
|
|
1361
|
+
)
|
|
1362
|
+
active = r"Literal \\`code` tick."
|
|
1363
|
+
assert any(
|
|
1364
|
+
segment.kind == "code_or_verbatim"
|
|
1365
|
+
for segment in screening.segment_document(active, "markdown")
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def test_latex_percent_escape_parity_is_respected() -> None:
|
|
1370
|
+
escaped = r"Literal \% text."
|
|
1371
|
+
assert all(
|
|
1372
|
+
segment.kind == "author_prose"
|
|
1373
|
+
for segment in screening.segment_document(escaped, "latex")
|
|
1374
|
+
)
|
|
1375
|
+
active = r"Literal \\% comment"
|
|
1376
|
+
assert any(
|
|
1377
|
+
segment.kind == "code_or_verbatim"
|
|
1378
|
+
for segment in screening.segment_document(active, "latex")
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def test_unclosed_construct_is_unknown_and_degrades_not_clean() -> None:
|
|
1383
|
+
report = screening.build_own_draft_report(
|
|
1384
|
+
"Ordinary prose `luminous turnip",
|
|
1385
|
+
artifact_id="broken.md",
|
|
1386
|
+
artifact_format="markdown",
|
|
1387
|
+
state=_state(),
|
|
1388
|
+
checked_at=CHECKED_AT,
|
|
1389
|
+
recorded_at=RECORDED_AT,
|
|
1390
|
+
)
|
|
1391
|
+
assert report["check_status"] == "degraded"
|
|
1392
|
+
assert report["finding"] == "unresolved"
|
|
1393
|
+
assert report["reason_code"] == "DOCUMENT_PARSE_DEGRADED"
|
|
1394
|
+
assert report["counts"]["unknown_segments"] > 0
|
|
1395
|
+
assert report["matches"]
|
|
1396
|
+
assert report["matches"][0]["disposition"] == "review_unknown_no_automatic_rewrite"
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def test_segment_resource_cap_degrades_without_partial_matches(
|
|
1400
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1401
|
+
) -> None:
|
|
1402
|
+
monkeypatch.setattr(screening, "MAX_SEGMENTS", 1)
|
|
1403
|
+
report = screening.build_own_draft_report(
|
|
1404
|
+
"prose `code` prose",
|
|
1405
|
+
artifact_id="many-segments.md",
|
|
1406
|
+
artifact_format="markdown",
|
|
1407
|
+
state=_state(),
|
|
1408
|
+
checked_at=CHECKED_AT,
|
|
1409
|
+
recorded_at=RECORDED_AT,
|
|
1410
|
+
)
|
|
1411
|
+
assert report["check_status"] == "degraded"
|
|
1412
|
+
assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
|
|
1413
|
+
assert report["matches"] == []
|
|
1414
|
+
assert report["counts"]["rules_evaluated"] == 0
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
@pytest.mark.parametrize(
|
|
1418
|
+
"text",
|
|
1419
|
+
[
|
|
1420
|
+
"a\n\nb\n\nc\n\n",
|
|
1421
|
+
"# References\none\ntwo\nthree\n",
|
|
1422
|
+
],
|
|
1423
|
+
)
|
|
1424
|
+
def test_partition_boundaries_fail_while_collecting(
|
|
1425
|
+
monkeypatch: pytest.MonkeyPatch, text: str
|
|
1426
|
+
) -> None:
|
|
1427
|
+
monkeypatch.setattr(screening, "MAX_SEGMENTS", 2)
|
|
1428
|
+
with pytest.raises(screening.MatchLimitError, match="document partition exceeds 2"):
|
|
1429
|
+
screening.segment_document(text, "markdown")
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
def test_over_token_input_fails_before_utf8_endpoint_projection(
|
|
1433
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1434
|
+
) -> None:
|
|
1435
|
+
bundle = _bundle()
|
|
1436
|
+
monkeypatch.setattr(screening, "MAX_TOKENS", 1)
|
|
1437
|
+
|
|
1438
|
+
def unexpected_offsets(_text, _indices):
|
|
1439
|
+
raise AssertionError("UTF-8 endpoint projection must follow token admission")
|
|
1440
|
+
|
|
1441
|
+
monkeypatch.setattr(screening, "_selected_byte_offsets", unexpected_offsets)
|
|
1442
|
+
with pytest.raises(screening.MatchLimitError, match="token count exceeds 1"):
|
|
1443
|
+
screening.scan_segments(
|
|
1444
|
+
"alpha beta",
|
|
1445
|
+
[screening.Segment("SEG-000001", "author_prose", 0, 10)],
|
|
1446
|
+
bundle,
|
|
1447
|
+
artifact_sha256=hashlib.sha256(b"alpha beta").hexdigest(),
|
|
1448
|
+
surface="own_draft",
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def test_raw_token_expansion_is_bounded_before_normalization(
|
|
1453
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1454
|
+
) -> None:
|
|
1455
|
+
monkeypatch.setattr(screening, "MAX_RAW_TOKEN_CODEPOINTS", 2)
|
|
1456
|
+
|
|
1457
|
+
def unexpected_normalization(_value: str):
|
|
1458
|
+
raise AssertionError("overlong raw token must fail before NFKC expansion")
|
|
1459
|
+
|
|
1460
|
+
monkeypatch.setattr(screening, "_normalized_token_parts", unexpected_normalization)
|
|
1461
|
+
with pytest.raises(screening.MatchLimitError, match="raw token exceeds 2"):
|
|
1462
|
+
screening.tokenize("\ufdfa" * 3)
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def test_raw_token_cap_accepts_n_and_rejects_n_plus_one(
|
|
1466
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1467
|
+
) -> None:
|
|
1468
|
+
monkeypatch.setattr(screening, "MAX_RAW_TOKEN_CODEPOINTS", 2)
|
|
1469
|
+
assert [item.normalized for item in screening.tokenize("ab")] == ["ab"]
|
|
1470
|
+
with pytest.raises(screening.MatchLimitError, match="raw token exceeds 2"):
|
|
1471
|
+
screening.tokenize("abc")
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def test_parser_work_budget_accepts_n_and_rejects_n_plus_one(
|
|
1475
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1476
|
+
) -> None:
|
|
1477
|
+
monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 2)
|
|
1478
|
+
budget = screening.ParseWorkBudget()
|
|
1479
|
+
budget.spend()
|
|
1480
|
+
budget.spend()
|
|
1481
|
+
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
|
|
1482
|
+
budget.spend()
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def test_escaped_and_doi_closer_candidates_spend_parser_budget(
|
|
1486
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1487
|
+
) -> None:
|
|
1488
|
+
monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 2)
|
|
1489
|
+
accepted = screening.ParseWorkBudget()
|
|
1490
|
+
assert screening._find_unescaped(r"\$$", "$", 0, budget=accepted) == 2
|
|
1491
|
+
exhausted = screening.ParseWorkBudget()
|
|
1492
|
+
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
|
|
1493
|
+
screening._find_unescaped(r"\$\$$", "$", 0, budget=exhausted)
|
|
1494
|
+
doi_budget = screening.ParseWorkBudget()
|
|
1495
|
+
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
|
|
1496
|
+
screening._doi_title_intervals(
|
|
1497
|
+
"[title](doi:10.1/example)",
|
|
1498
|
+
excluded=(),
|
|
1499
|
+
budget=doi_budget,
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
@pytest.mark.parametrize(
|
|
1504
|
+
("text", "artifact_format"),
|
|
1505
|
+
[
|
|
1506
|
+
("<!--\n> one\n> two\n> three\n-->", "markdown"),
|
|
1507
|
+
(r"\end{quote}\end{quote}\end{quote}\end{quote}", "latex"),
|
|
1508
|
+
],
|
|
1509
|
+
)
|
|
1510
|
+
def test_excluded_and_unmatched_context_candidates_spend_parser_budget(
|
|
1511
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1512
|
+
text: str,
|
|
1513
|
+
artifact_format: str,
|
|
1514
|
+
) -> None:
|
|
1515
|
+
monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 3)
|
|
1516
|
+
with pytest.raises(screening.MatchLimitError, match="parser work exceeds 3"):
|
|
1517
|
+
screening.segment_document(text, artifact_format)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def test_selected_utf8_offsets_are_exact_and_endpoint_bounded() -> None:
|
|
1521
|
+
text = "A台灣B"
|
|
1522
|
+
assert screening._selected_byte_offsets(text, {0, 1, 3, 4}) == {
|
|
1523
|
+
0: 0,
|
|
1524
|
+
1: 1,
|
|
1525
|
+
3: 7,
|
|
1526
|
+
4: 8,
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def test_delimiter_parser_enforces_interval_cap_before_return(
|
|
1531
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1532
|
+
) -> None:
|
|
1533
|
+
monkeypatch.setattr(screening, "MAX_PARSE_INTERVALS", 2)
|
|
1534
|
+
with pytest.raises(screening.MatchLimitError, match="parse interval count exceeds 2"):
|
|
1535
|
+
screening._paired_delimiter_intervals(
|
|
1536
|
+
"$one$ $two$ $three$", "$", "$", "code_or_verbatim"
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def test_rule_by_segment_work_is_globally_bounded(
|
|
1541
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1542
|
+
) -> None:
|
|
1543
|
+
monkeypatch.setattr(screening, "MAX_RULE_SEGMENT_EVALUATIONS", 1)
|
|
1544
|
+
with pytest.raises(
|
|
1545
|
+
screening.MatchLimitError, match="rule-by-segment evaluations exceed 1"
|
|
1546
|
+
):
|
|
1547
|
+
screening.scan_segments(
|
|
1548
|
+
"luminous turnip",
|
|
1549
|
+
[screening.Segment("SEG-000001", "author_prose", 0, 16)],
|
|
1550
|
+
_bundle(),
|
|
1551
|
+
artifact_sha256=hashlib.sha256(b"luminous turnip").hexdigest(),
|
|
1552
|
+
surface="own_draft",
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def test_literal_and_composition_work_budget_is_shared_across_corpus(
|
|
1557
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1558
|
+
) -> None:
|
|
1559
|
+
monkeypatch.setattr(screening, "MAX_MATCH_WORK_UNITS", 300)
|
|
1560
|
+
output = _enriched()
|
|
1561
|
+
first, second = output["literature_corpus"][:2]
|
|
1562
|
+
assert all(
|
|
1563
|
+
signal["check_status"] == "checked"
|
|
1564
|
+
for signal in first["bibliographic_integrity_signals"]
|
|
1565
|
+
)
|
|
1566
|
+
second_title = next(
|
|
1567
|
+
signal
|
|
1568
|
+
for signal in second["bibliographic_integrity_signals"]
|
|
1569
|
+
if signal["tortured_phrase_context"]["surface"] == "cited_title"
|
|
1570
|
+
)
|
|
1571
|
+
assert second_title["check_status"] == "degraded"
|
|
1572
|
+
assert (
|
|
1573
|
+
second_title["tortured_phrase_context"]["reason_code"]
|
|
1574
|
+
== "MATCH_RESOURCE_LIMIT"
|
|
1575
|
+
)
|
|
1576
|
+
assert second_title["tortured_phrase_context"]["matches"] == []
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def test_report_digest_and_full_replay() -> None:
|
|
1580
|
+
report = _report()
|
|
1581
|
+
screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
|
|
1582
|
+
report["counts"]["rule_match_count"] += 1
|
|
1583
|
+
with pytest.raises(screening.ScreeningError, match="report_sha256"):
|
|
1584
|
+
screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def test_rehashed_report_still_fails_source_replay() -> None:
|
|
1588
|
+
report = _report()
|
|
1589
|
+
report["matches"][0]["matched_text"] = "forged"
|
|
1590
|
+
report["matches"][0]["matched_text_sha256"] = screening._sha256_text("forged")
|
|
1591
|
+
report["report_sha256"] = screening._report_digest(report)
|
|
1592
|
+
with pytest.raises(screening.ScreeningError, match="replay"):
|
|
1593
|
+
screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
def test_oversized_full_report_collapses_to_a_self_readable_degraded_artifact(
|
|
1597
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1598
|
+
) -> None:
|
|
1599
|
+
text = " ".join(["luminous turnip"] * 100)
|
|
1600
|
+
monkeypatch.setattr(screening, "MAX_ADVISORY_BYTES", 5000)
|
|
1601
|
+
report = screening.build_own_draft_report(
|
|
1602
|
+
text,
|
|
1603
|
+
artifact_id="bounded.md",
|
|
1604
|
+
artifact_format="markdown",
|
|
1605
|
+
state=_state(),
|
|
1606
|
+
checked_at=CHECKED_AT,
|
|
1607
|
+
recorded_at=RECORDED_AT,
|
|
1608
|
+
)
|
|
1609
|
+
assert report["check_status"] == "degraded"
|
|
1610
|
+
assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
|
|
1611
|
+
assert report["matches"] == []
|
|
1612
|
+
assert len(screening._pretty_json_bytes(report)) <= screening.MAX_ADVISORY_BYTES
|
|
1613
|
+
path = tmp_path / "report.json"
|
|
1614
|
+
screening._atomic_write_json(
|
|
1615
|
+
path, report, maximum=screening.MAX_ADVISORY_BYTES
|
|
1616
|
+
)
|
|
1617
|
+
assert screening._load_report(path) == report
|
|
1618
|
+
screening.validate_own_draft_report(report, text, state=_state())
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
def test_bounded_reader_rejects_before_consuming_beyond_limit(tmp_path: Path) -> None:
|
|
1622
|
+
path = tmp_path / "oversized.bin"
|
|
1623
|
+
path.write_bytes(b"x" * 9)
|
|
1624
|
+
with pytest.raises(screening.MatchLimitError, match="exceeds 8 bytes"):
|
|
1625
|
+
screening._read_bounded_bytes(path, maximum=8)
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
def test_no_snapshot_is_explicit_not_checked() -> None:
|
|
1629
|
+
state = screening.snapshot_state(None, None)
|
|
1630
|
+
report = screening.build_own_draft_report(
|
|
1631
|
+
"luminous turnip",
|
|
1632
|
+
artifact_id="draft.md",
|
|
1633
|
+
artifact_format="markdown",
|
|
1634
|
+
state=state,
|
|
1635
|
+
checked_at=CHECKED_AT,
|
|
1636
|
+
recorded_at=RECORDED_AT,
|
|
1637
|
+
)
|
|
1638
|
+
assert report["check_status"] == "not_checked"
|
|
1639
|
+
assert report["finding"] == "unresolved"
|
|
1640
|
+
assert report["reason_code"] == "SNAPSHOT_NOT_PROVIDED"
|
|
1641
|
+
assert report["matches"] == []
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
@pytest.mark.parametrize(
|
|
1645
|
+
"state",
|
|
1646
|
+
[
|
|
1647
|
+
screening.snapshot_state(None, None),
|
|
1648
|
+
screening.snapshot_state(FIXTURES / "snapshot.json", None),
|
|
1649
|
+
],
|
|
1650
|
+
)
|
|
1651
|
+
def test_empty_draft_preserves_missing_or_degraded_snapshot_authority(
|
|
1652
|
+
state: screening.SnapshotState,
|
|
1653
|
+
) -> None:
|
|
1654
|
+
report = screening.build_own_draft_report(
|
|
1655
|
+
"",
|
|
1656
|
+
artifact_id="empty.md",
|
|
1657
|
+
artifact_format="markdown",
|
|
1658
|
+
state=state,
|
|
1659
|
+
checked_at=CHECKED_AT,
|
|
1660
|
+
recorded_at=RECORDED_AT,
|
|
1661
|
+
)
|
|
1662
|
+
assert report["check_status"] == (
|
|
1663
|
+
"not_checked" if state.status == "not_checked" else "degraded"
|
|
1664
|
+
)
|
|
1665
|
+
assert report["reason_code"] == state.reason_code
|
|
1666
|
+
assert report["finding"] == "unresolved"
|
|
1667
|
+
screening.validate_own_draft_report(report, "", state=state)
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def test_empty_draft_is_degraded_never_a_zero_match() -> None:
|
|
1671
|
+
report = screening.build_own_draft_report(
|
|
1672
|
+
" \t\n",
|
|
1673
|
+
artifact_id="empty.md",
|
|
1674
|
+
artifact_format="markdown",
|
|
1675
|
+
state=_state(),
|
|
1676
|
+
checked_at=CHECKED_AT,
|
|
1677
|
+
recorded_at=RECORDED_AT,
|
|
1678
|
+
)
|
|
1679
|
+
assert report["check_status"] == "degraded"
|
|
1680
|
+
assert report["finding"] == "unresolved"
|
|
1681
|
+
assert report["reason_code"] == "DOCUMENT_EMPTY"
|
|
1682
|
+
assert report["matches"] == []
|
|
1683
|
+
assert report["counts"]["rules_evaluated"] == 0
|
|
1684
|
+
|
|
1685
|
+
|
|
1686
|
+
def test_snapshot_byte_tamper_fails_hash_before_content_access() -> None:
|
|
1687
|
+
manifest = FIXTURES / "snapshot_manifest.json"
|
|
1688
|
+
state = screening.snapshot_state(FIXTURES / "own_draft.md", manifest)
|
|
1689
|
+
assert state.status == "degraded"
|
|
1690
|
+
assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
@pytest.mark.parametrize(
|
|
1694
|
+
("raw", "reason"),
|
|
1695
|
+
[
|
|
1696
|
+
(b"\xef\xbb\xbf{}", "SNAPSHOT_BYTES_INVALID"),
|
|
1697
|
+
(b"\xff", "SNAPSHOT_BYTES_INVALID"),
|
|
1698
|
+
(b'{"schema_version":"a","schema_version":"b"}', "SNAPSHOT_BYTES_INVALID"),
|
|
1699
|
+
(b'{"x":NaN}', "SNAPSHOT_BYTES_INVALID"),
|
|
1700
|
+
],
|
|
1701
|
+
)
|
|
1702
|
+
def test_strict_snapshot_loader_rejects_unsafe_bytes(
|
|
1703
|
+
tmp_path: Path, raw: bytes, reason: str
|
|
1704
|
+
) -> None:
|
|
1705
|
+
snapshot_path = tmp_path / "bad.json"
|
|
1706
|
+
snapshot_path.write_bytes(raw)
|
|
1707
|
+
manifest = json.loads(
|
|
1708
|
+
(FIXTURES / "snapshot_manifest.json").read_text(encoding="utf-8")
|
|
1709
|
+
)
|
|
1710
|
+
manifest["snapshot_sha256"] = hashlib.sha256(raw).hexdigest()
|
|
1711
|
+
manifest_path = tmp_path / "manifest.json"
|
|
1712
|
+
_strict_dump(manifest_path, manifest)
|
|
1713
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1714
|
+
assert state.status == "degraded"
|
|
1715
|
+
assert state.reason_code == reason
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
def test_draft_reader_rejects_isolated_carriage_return(tmp_path: Path) -> None:
|
|
1719
|
+
draft = tmp_path / "draft.md"
|
|
1720
|
+
draft.write_bytes(b"alpha\rbeta")
|
|
1721
|
+
with pytest.raises(screening.ScreeningError, match="isolated carriage return"):
|
|
1722
|
+
screening._read_strict_text(
|
|
1723
|
+
draft, maximum=screening.MAX_DOCUMENT_BYTES, label="draft"
|
|
1724
|
+
)
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
def test_direct_builder_allows_crlf_but_rejects_isolated_carriage_return() -> None:
|
|
1728
|
+
screening.build_own_draft_report(
|
|
1729
|
+
"alpha\r\nbeta",
|
|
1730
|
+
artifact_id="draft.md",
|
|
1731
|
+
artifact_format="markdown",
|
|
1732
|
+
state=_state(),
|
|
1733
|
+
checked_at=CHECKED_AT,
|
|
1734
|
+
recorded_at=RECORDED_AT,
|
|
1735
|
+
)
|
|
1736
|
+
with pytest.raises(screening.ScreeningError, match="isolated carriage return"):
|
|
1737
|
+
screening.build_own_draft_report(
|
|
1738
|
+
"alpha\rbeta",
|
|
1739
|
+
artifact_id="draft.md",
|
|
1740
|
+
artifact_format="markdown",
|
|
1741
|
+
state=_state(),
|
|
1742
|
+
checked_at=CHECKED_AT,
|
|
1743
|
+
recorded_at=RECORDED_AT,
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
def test_manifest_hash_mismatch_never_checks_remaining_rules(tmp_path: Path) -> None:
|
|
1748
|
+
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
1749
|
+
snapshot["rules"][0]["expression"]["value"] = "changed phrase"
|
|
1750
|
+
_strict_dump(snapshot_path, snapshot)
|
|
1751
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1752
|
+
assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
def test_manifest_snapshot_identity_must_match_hash_bound_snapshot(
|
|
1756
|
+
tmp_path: Path,
|
|
1757
|
+
) -> None:
|
|
1758
|
+
snapshot_path, manifest_path, _snapshot, manifest = _snapshot_pair(tmp_path)
|
|
1759
|
+
manifest["snapshot_id"] = "forged-snapshot-id"
|
|
1760
|
+
_strict_dump(manifest_path, manifest)
|
|
1761
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1762
|
+
assert state.status == "degraded"
|
|
1763
|
+
assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
@pytest.mark.parametrize(
|
|
1767
|
+
"mutation",
|
|
1768
|
+
[
|
|
1769
|
+
"synthetic_reference",
|
|
1770
|
+
"user_without_declaration",
|
|
1771
|
+
"permission_without_reference",
|
|
1772
|
+
"unresolved_but_permitted",
|
|
1773
|
+
"null_source_locator",
|
|
1774
|
+
],
|
|
1775
|
+
)
|
|
1776
|
+
def test_manifest_rights_and_source_projection_fail_closed(
|
|
1777
|
+
tmp_path: Path, mutation: str
|
|
1778
|
+
) -> None:
|
|
1779
|
+
snapshot_path, manifest_path, _snapshot, manifest = _snapshot_pair(tmp_path)
|
|
1780
|
+
if mutation == "synthetic_reference":
|
|
1781
|
+
manifest["rights"]["reference"] = "unexpected reference"
|
|
1782
|
+
elif mutation == "user_without_declaration":
|
|
1783
|
+
manifest["supply_mode"] = "user_supplied"
|
|
1784
|
+
manifest["rights"] = {
|
|
1785
|
+
"basis": "user_declared_authorized",
|
|
1786
|
+
"redistribution_status": "permitted",
|
|
1787
|
+
"reference": None,
|
|
1788
|
+
"user_declaration": None,
|
|
1789
|
+
}
|
|
1790
|
+
elif mutation == "permission_without_reference":
|
|
1791
|
+
manifest["supply_mode"] = "user_supplied"
|
|
1792
|
+
manifest["rights"] = {
|
|
1793
|
+
"basis": "written_permission",
|
|
1794
|
+
"redistribution_status": "permitted",
|
|
1795
|
+
"reference": None,
|
|
1796
|
+
"user_declaration": None,
|
|
1797
|
+
}
|
|
1798
|
+
elif mutation == "unresolved_but_permitted":
|
|
1799
|
+
manifest["supply_mode"] = "user_supplied"
|
|
1800
|
+
manifest["rights"] = {
|
|
1801
|
+
"basis": "unresolved",
|
|
1802
|
+
"redistribution_status": "permitted",
|
|
1803
|
+
"reference": None,
|
|
1804
|
+
"user_declaration": None,
|
|
1805
|
+
}
|
|
1806
|
+
else:
|
|
1807
|
+
manifest["source"]["locator"] = None
|
|
1808
|
+
_strict_dump(manifest_path, manifest)
|
|
1809
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1810
|
+
assert state.status == "degraded"
|
|
1811
|
+
assert state.reason_code == "SNAPSHOT_MANIFEST_INVALID"
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def test_advisory_schema_mirrors_manifest_rights_conditionals() -> None:
|
|
1815
|
+
report = _report()
|
|
1816
|
+
validator = Draft202012Validator(
|
|
1817
|
+
screening._load_schema(screening.ADVISORY_SCHEMA_PATH),
|
|
1818
|
+
format_checker=Draft202012Validator.FORMAT_CHECKER,
|
|
1819
|
+
)
|
|
1820
|
+
rights = report["input_binding"]["snapshot"]["rights"]
|
|
1821
|
+
rights["basis"] = "user_declared_authorized"
|
|
1822
|
+
rights["user_declaration"] = None
|
|
1823
|
+
assert list(validator.iter_errors(report))
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def test_explicit_but_unreadable_snapshot_is_degraded_not_absent(
|
|
1827
|
+
tmp_path: Path,
|
|
1828
|
+
) -> None:
|
|
1829
|
+
missing = tmp_path / "missing-snapshot.json"
|
|
1830
|
+
state = screening.snapshot_state(missing, FIXTURES / "snapshot_manifest.json")
|
|
1831
|
+
assert state.status == "degraded"
|
|
1832
|
+
assert state.reason_code == "SNAPSHOT_BYTES_INVALID"
|
|
1833
|
+
signal = screening.build_cited_signal(
|
|
1834
|
+
_passport()["literature_corpus"][0],
|
|
1835
|
+
surface="cited_title",
|
|
1836
|
+
state=state,
|
|
1837
|
+
checked_at=CHECKED_AT,
|
|
1838
|
+
recorded_at=RECORDED_AT,
|
|
1839
|
+
)
|
|
1840
|
+
assert signal["check_status"] == "degraded"
|
|
1841
|
+
assert signal["tortured_phrase_context"]["reason_code"] == "SNAPSHOT_BYTES_INVALID"
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
@pytest.mark.parametrize(
|
|
1845
|
+
("mutation", "expected_reason"),
|
|
1846
|
+
[
|
|
1847
|
+
(lambda snap, man: snap.update({"grammar_profile": "unknown/9"}), "SNAPSHOT_PROFILE_UNSUPPORTED"),
|
|
1848
|
+
(lambda snap, man: snap.update({"rules": []}), "SNAPSHOT_MANIFEST_INVALID"),
|
|
1849
|
+
(
|
|
1850
|
+
lambda snap, man: snap["rules"][0].update(
|
|
1851
|
+
{"expression": {"op": "regex", "value": ".*"}}
|
|
1852
|
+
),
|
|
1853
|
+
"SNAPSHOT_RULES_UNSUPPORTED",
|
|
1854
|
+
),
|
|
1855
|
+
(
|
|
1856
|
+
lambda snap, man: snap["rules"].append(copy.deepcopy(snap["rules"][0])),
|
|
1857
|
+
"SNAPSHOT_RULES_UNSUPPORTED",
|
|
1858
|
+
),
|
|
1859
|
+
(
|
|
1860
|
+
lambda snap, man: man.update({"unsupported_rule_count": 1}),
|
|
1861
|
+
"SNAPSHOT_RULES_UNSUPPORTED",
|
|
1862
|
+
),
|
|
1863
|
+
],
|
|
1864
|
+
)
|
|
1865
|
+
def test_invalid_or_partial_grammar_is_never_clean(
|
|
1866
|
+
tmp_path: Path, mutation, expected_reason: str
|
|
1867
|
+
) -> None:
|
|
1868
|
+
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
1869
|
+
mutation(snapshot, manifest)
|
|
1870
|
+
_rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
|
|
1871
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1872
|
+
assert state.status == "degraded"
|
|
1873
|
+
assert state.reason_code == expected_reason
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
@pytest.mark.parametrize(
|
|
1877
|
+
"injected_value",
|
|
1878
|
+
["grammar_profile", "normalizer_profile", "schema_version"],
|
|
1879
|
+
)
|
|
1880
|
+
def test_invalid_rule_text_cannot_inject_profile_failure_reason(
|
|
1881
|
+
tmp_path: Path, injected_value: str
|
|
1882
|
+
) -> None:
|
|
1883
|
+
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
1884
|
+
snapshot["rules"][0]["expression"] = {
|
|
1885
|
+
"op": "regex",
|
|
1886
|
+
"value": injected_value,
|
|
1887
|
+
}
|
|
1888
|
+
_rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
|
|
1889
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1890
|
+
assert state.status == "degraded"
|
|
1891
|
+
assert state.reason_code == "SNAPSHOT_RULES_UNSUPPORTED"
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def test_semantically_duplicate_rule_is_rejected(tmp_path: Path) -> None:
|
|
1895
|
+
snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
|
|
1896
|
+
duplicate = copy.deepcopy(snapshot["rules"][0])
|
|
1897
|
+
duplicate["rule_id"] = "different_id_same_semantics"
|
|
1898
|
+
snapshot["rules"].append(duplicate)
|
|
1899
|
+
_rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
|
|
1900
|
+
state = screening.snapshot_state(snapshot_path, manifest_path)
|
|
1901
|
+
assert state.reason_code == "SNAPSHOT_RULES_UNSUPPORTED"
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
def test_near_gap_is_intervening_token_count_and_ordered() -> None:
|
|
1905
|
+
rule = next(item for item in _bundle().rules if item.rule_id == "syn_near_ordered")
|
|
1906
|
+
assert screening.evaluate_rule(rule, screening.tokenize("nebula x cabbage"))
|
|
1907
|
+
assert not screening.evaluate_rule(rule, screening.tokenize("nebula x y cabbage"))
|
|
1908
|
+
assert not screening.evaluate_rule(rule, screening.tokenize("cabbage x nebula"))
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def test_near_never_crosses_context_segments() -> None:
|
|
1912
|
+
report = screening.build_own_draft_report(
|
|
1913
|
+
"nebula `x` cabbage",
|
|
1914
|
+
artifact_id="segments.md",
|
|
1915
|
+
artifact_format="markdown",
|
|
1916
|
+
state=_state(),
|
|
1917
|
+
checked_at=CHECKED_AT,
|
|
1918
|
+
recorded_at=RECORDED_AT,
|
|
1919
|
+
)
|
|
1920
|
+
assert all(item["pattern_id"] != "syn_near_ordered" for item in report["matches"])
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
def test_explicit_timestamps_only_no_ambient_clock_source() -> None:
|
|
1924
|
+
source = (SCRIPTS / "tortured_phrase_screening.py").read_text(encoding="utf-8")
|
|
1925
|
+
assert ".now(" not in source
|
|
1926
|
+
with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
|
|
1927
|
+
screening.build_own_draft_report(
|
|
1928
|
+
"luminous turnip",
|
|
1929
|
+
artifact_id="draft.md",
|
|
1930
|
+
artifact_format="markdown",
|
|
1931
|
+
state=_state(),
|
|
1932
|
+
checked_at="today",
|
|
1933
|
+
recorded_at=RECORDED_AT,
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
def test_recorded_at_cannot_precede_checked_at() -> None:
|
|
1938
|
+
with pytest.raises(screening.ScreeningError, match="must not precede"):
|
|
1939
|
+
screening.build_own_draft_report(
|
|
1940
|
+
"luminous turnip",
|
|
1941
|
+
artifact_id="draft.md",
|
|
1942
|
+
artifact_format="markdown",
|
|
1943
|
+
state=_state(),
|
|
1944
|
+
checked_at="2026-08-10T01:00:01Z",
|
|
1945
|
+
recorded_at="2026-08-10T01:00:00Z",
|
|
1946
|
+
)
|
|
1947
|
+
entry = _passport()["literature_corpus"][0]
|
|
1948
|
+
with pytest.raises(screening.ScreeningError, match="must not precede"):
|
|
1949
|
+
screening.build_cited_signal(
|
|
1950
|
+
entry,
|
|
1951
|
+
surface="cited_title",
|
|
1952
|
+
state=_state(),
|
|
1953
|
+
checked_at="2026-08-10T01:00:01Z",
|
|
1954
|
+
recorded_at="2026-08-10T01:00:00Z",
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
def test_submicrosecond_timestamp_order_cannot_be_truncated() -> None:
|
|
1959
|
+
checked_at = "2026-08-10T01:00:00.0000009Z"
|
|
1960
|
+
recorded_at = "2026-08-10T01:00:00.0000001Z"
|
|
1961
|
+
with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
|
|
1962
|
+
screening.build_own_draft_report(
|
|
1963
|
+
"luminous turnip",
|
|
1964
|
+
artifact_id="draft.md",
|
|
1965
|
+
artifact_format="markdown",
|
|
1966
|
+
state=_state(),
|
|
1967
|
+
checked_at=checked_at,
|
|
1968
|
+
recorded_at=recorded_at,
|
|
1969
|
+
)
|
|
1970
|
+
with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
|
|
1971
|
+
screening.build_cited_signal(
|
|
1972
|
+
_passport()["literature_corpus"][0],
|
|
1973
|
+
surface="cited_title",
|
|
1974
|
+
state=_state(),
|
|
1975
|
+
checked_at=checked_at,
|
|
1976
|
+
recorded_at=recorded_at,
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def test_renderer_is_one_page_bounded_and_has_claim_ceiling() -> None:
|
|
1981
|
+
report = _report()
|
|
1982
|
+
rendered = screening.render_own_draft_report(
|
|
1983
|
+
report, _draft("own_draft.md"), state=_state()
|
|
1984
|
+
)
|
|
1985
|
+
assert screening.SUMMARY_LABEL in rendered
|
|
1986
|
+
assert "does not establish papermill, AI, or author origin" in rendered
|
|
1987
|
+
assert "not a clean-text certificate" in rendered
|
|
1988
|
+
assert "one fixed page capped at 25" in rendered
|
|
1989
|
+
assert "--all" not in rendered
|
|
1990
|
+
|
|
1991
|
+
|
|
1992
|
+
def test_renderer_reports_omitted_count_and_machine_replay_key() -> None:
|
|
1993
|
+
text = " ".join(["luminous turnip"] * 30)
|
|
1994
|
+
report = screening.build_own_draft_report(
|
|
1995
|
+
text,
|
|
1996
|
+
artifact_id="many-matches.md",
|
|
1997
|
+
artifact_format="markdown",
|
|
1998
|
+
state=_state(),
|
|
1999
|
+
checked_at=CHECKED_AT,
|
|
2000
|
+
recorded_at=RECORDED_AT,
|
|
2001
|
+
)
|
|
2002
|
+
assert len(report["matches"]) > screening.MAX_RENDER_PAGE_SIZE
|
|
2003
|
+
rendered = screening.render_own_draft_report(report, text, state=_state())
|
|
2004
|
+
omitted = len(report["matches"]) - screening.MAX_RENDER_PAGE_SIZE
|
|
2005
|
+
assert f"omitted {omitted}" in rendered
|
|
2006
|
+
assert "Complete machine JSON replay key" in rendered
|
|
2007
|
+
assert "artifact_id=many-matches.md" in rendered
|
|
2008
|
+
assert report["report_sha256"] in rendered
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def test_renderer_uses_outcome_specific_zero_and_unresolved_wording() -> None:
|
|
2012
|
+
zero = screening.build_own_draft_report(
|
|
2013
|
+
"ordinary unmatched prose",
|
|
2014
|
+
artifact_id="zero.md",
|
|
2015
|
+
artifact_format="markdown",
|
|
2016
|
+
state=_state(),
|
|
2017
|
+
checked_at=CHECKED_AT,
|
|
2018
|
+
recorded_at=RECORDED_AT,
|
|
2019
|
+
)
|
|
2020
|
+
rendered_zero = screening.render_own_draft_report(
|
|
2021
|
+
zero, "ordinary unmatched prose", state=_state()
|
|
2022
|
+
)
|
|
2023
|
+
assert screening.SUMMARY_LABEL not in rendered_zero
|
|
2024
|
+
assert "No phrase-list match observed on the checked surface" in rendered_zero
|
|
2025
|
+
assert "not a clean-text certificate" in rendered_zero
|
|
2026
|
+
|
|
2027
|
+
unresolved_state = screening.snapshot_state(None, None)
|
|
2028
|
+
unresolved = screening.build_own_draft_report(
|
|
2029
|
+
"ordinary unmatched prose",
|
|
2030
|
+
artifact_id="unresolved.md",
|
|
2031
|
+
artifact_format="markdown",
|
|
2032
|
+
state=unresolved_state,
|
|
2033
|
+
checked_at=CHECKED_AT,
|
|
2034
|
+
recorded_at=RECORDED_AT,
|
|
2035
|
+
)
|
|
2036
|
+
rendered_unresolved = screening.render_own_draft_report(
|
|
2037
|
+
unresolved, "ordinary unmatched prose", state=unresolved_state
|
|
2038
|
+
)
|
|
2039
|
+
assert screening.SUMMARY_LABEL not in rendered_unresolved
|
|
2040
|
+
assert "screening is unresolved" in rendered_unresolved
|
|
2041
|
+
|
|
2042
|
+
|
|
2043
|
+
def test_renderer_escapes_markdown_and_html_injection() -> None:
|
|
2044
|
+
assert "<img" not in screening._markdown_cell("<img src=x>")
|
|
2045
|
+
assert "\\[click\\]" in screening._markdown_cell("[click](https://example.invalid)")
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def test_corpus_rows_are_per_surface_and_missing_abstract_is_explicit() -> None:
|
|
2049
|
+
output = _enriched()
|
|
2050
|
+
by_key = {item["citation_key"]: item for item in output["literature_corpus"]}
|
|
2051
|
+
complete = by_key["fixture_complete_2026"]["bibliographic_integrity_signals"]
|
|
2052
|
+
assert {item["tortured_phrase_context"]["surface"] for item in complete} == {
|
|
2053
|
+
"cited_title",
|
|
2054
|
+
"cited_abstract",
|
|
2055
|
+
}
|
|
2056
|
+
missing = by_key["fixture_missing_2026"]["bibliographic_integrity_signals"]
|
|
2057
|
+
abstract = next(
|
|
2058
|
+
item for item in missing if item["tortured_phrase_context"]["surface"] == "cited_abstract"
|
|
2059
|
+
)
|
|
2060
|
+
title = next(
|
|
2061
|
+
item for item in missing if item["tortured_phrase_context"]["surface"] == "cited_title"
|
|
2062
|
+
)
|
|
2063
|
+
assert title["finding"] == "detected"
|
|
2064
|
+
assert abstract["check_status"] == "not_checked"
|
|
2065
|
+
assert abstract["finding"] == "unresolved"
|
|
2066
|
+
assert abstract["tortured_phrase_context"]["reason_code"] == "ABSTRACT_MISSING"
|
|
2067
|
+
assert abstract["tortured_phrase_context"]["counts"]["rules_evaluated"] == 0
|
|
2068
|
+
|
|
2069
|
+
|
|
2070
|
+
def test_whitespace_abstract_is_explicitly_empty_not_checked() -> None:
|
|
2071
|
+
document = _passport()
|
|
2072
|
+
document["literature_corpus"][0]["abstract"] = " \t\n"
|
|
2073
|
+
output = screening.enrich_passport(
|
|
2074
|
+
document,
|
|
2075
|
+
state=_state(),
|
|
2076
|
+
checked_at=CHECKED_AT,
|
|
2077
|
+
recorded_at=RECORDED_AT,
|
|
2078
|
+
)
|
|
2079
|
+
abstract = next(
|
|
2080
|
+
item
|
|
2081
|
+
for item in output["literature_corpus"][0]["bibliographic_integrity_signals"]
|
|
2082
|
+
if item["tortured_phrase_context"]["surface"] == "cited_abstract"
|
|
2083
|
+
)
|
|
2084
|
+
assert abstract["check_status"] == "not_checked"
|
|
2085
|
+
assert abstract["finding"] == "unresolved"
|
|
2086
|
+
assert abstract["tortured_phrase_context"]["reason_code"] == "ABSTRACT_EMPTY"
|
|
2087
|
+
assert abstract["tortured_phrase_context"]["surface_binding"] == {
|
|
2088
|
+
"content_sha256": None,
|
|
2089
|
+
"content_utf8_bytes": None,
|
|
2090
|
+
}
|
|
2091
|
+
assert abstract["tortured_phrase_context"]["counts"]["rules_evaluated"] == 0
|
|
2092
|
+
|
|
2093
|
+
|
|
2094
|
+
def test_legitimate_negative_only_means_no_configured_match() -> None:
|
|
2095
|
+
output = _enriched()
|
|
2096
|
+
entry = next(
|
|
2097
|
+
item for item in output["literature_corpus"] if item["citation_key"] == "fixture_negative_2026"
|
|
2098
|
+
)
|
|
2099
|
+
assert all(
|
|
2100
|
+
signal["finding"] == "not_detected"
|
|
2101
|
+
for signal in entry["bibliographic_integrity_signals"]
|
|
2102
|
+
)
|
|
2103
|
+
assert all(
|
|
2104
|
+
signal["tortured_phrase_context"]["boundary"]["absence_is_clean_certificate"]
|
|
2105
|
+
is False
|
|
2106
|
+
for signal in entry["bibliographic_integrity_signals"]
|
|
2107
|
+
)
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
def test_manual_entries_are_not_exempt_and_pointer_is_not_dereferenced() -> None:
|
|
2111
|
+
output = _enriched()
|
|
2112
|
+
entry = output["literature_corpus"][0]
|
|
2113
|
+
assert entry["obtained_via"] == "manual"
|
|
2114
|
+
assert entry["source_pointer"].startswith("fixture://")
|
|
2115
|
+
assert len(entry["bibliographic_integrity_signals"]) == 2
|
|
2116
|
+
|
|
2117
|
+
|
|
2118
|
+
def test_enrichment_is_semantically_idempotent_and_supersedes_current_row() -> None:
|
|
2119
|
+
first = _enriched()
|
|
2120
|
+
second = screening.enrich_passport(
|
|
2121
|
+
first,
|
|
2122
|
+
state=_state(),
|
|
2123
|
+
checked_at=CHECKED_AT,
|
|
2124
|
+
recorded_at=RECORDED_AT,
|
|
2125
|
+
)
|
|
2126
|
+
assert second == first
|
|
2127
|
+
first["literature_corpus"][0]["title"] += " changed"
|
|
2128
|
+
third = screening.enrich_passport(
|
|
2129
|
+
first,
|
|
2130
|
+
state=_state(),
|
|
2131
|
+
checked_at=CHECKED_AT,
|
|
2132
|
+
recorded_at=RECORDED_AT,
|
|
2133
|
+
)
|
|
2134
|
+
signals = third["literature_corpus"][0]["bibliographic_integrity_signals"]
|
|
2135
|
+
assert len([item for item in signals if item["schema_version"] == screening.SIGNAL_VERSION]) == 2
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def test_enrichment_rejects_foreign_current_row_before_supersession() -> None:
|
|
2139
|
+
document = _enriched()
|
|
2140
|
+
row = document["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
2141
|
+
row["subject"]["source_pointer"] = "fixture://foreign-source"
|
|
2142
|
+
with pytest.raises(screening.ScreeningError, match="source_pointer does not belong"):
|
|
2143
|
+
screening.enrich_passport(
|
|
2144
|
+
document,
|
|
2145
|
+
state=_state(),
|
|
2146
|
+
checked_at=CHECKED_AT,
|
|
2147
|
+
recorded_at=RECORDED_AT,
|
|
2148
|
+
)
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
def test_legacy_and_unrelated_signals_are_preserved() -> None:
|
|
2152
|
+
document = _passport()
|
|
2153
|
+
legacy = json.loads(
|
|
2154
|
+
(SCRIPTS / "fixtures/bibliographic_integrity_signals/tortured_phrase.json").read_text(
|
|
2155
|
+
encoding="utf-8"
|
|
2156
|
+
)
|
|
2157
|
+
)
|
|
2158
|
+
legacy["subject"]["citation_key"] = document["literature_corpus"][0]["citation_key"]
|
|
2159
|
+
legacy["subject"]["source_pointer"] = document["literature_corpus"][0]["source_pointer"]
|
|
2160
|
+
document["literature_corpus"][0]["bibliographic_integrity_signals"] = [legacy]
|
|
2161
|
+
output = screening.enrich_passport(
|
|
2162
|
+
document,
|
|
2163
|
+
state=_state(),
|
|
2164
|
+
checked_at=CHECKED_AT,
|
|
2165
|
+
recorded_at=RECORDED_AT,
|
|
2166
|
+
)
|
|
2167
|
+
signals = output["literature_corpus"][0]["bibliographic_integrity_signals"]
|
|
2168
|
+
assert signals[0] == legacy
|
|
2169
|
+
assert len(signals) == 3
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
def test_multiple_current_rows_are_rejected() -> None:
|
|
2173
|
+
document = _enriched()
|
|
2174
|
+
entry = document["literature_corpus"][0]
|
|
2175
|
+
duplicate = copy.deepcopy(entry["bibliographic_integrity_signals"][0])
|
|
2176
|
+
context = duplicate["tortured_phrase_context"]
|
|
2177
|
+
context["snapshot"]["snapshot_sha256"] = "0" * 64
|
|
2178
|
+
context["snapshot"]["manifest_sha256"] = "1" * 64
|
|
2179
|
+
duplicate["provenance"]["source_sha256"] = "0" * 64
|
|
2180
|
+
duplicate["signal_id"] = screening._signal_id(
|
|
2181
|
+
entry["citation_key"],
|
|
2182
|
+
context["surface"],
|
|
2183
|
+
context["snapshot"]["snapshot_sha256"],
|
|
2184
|
+
context["surface_binding"]["content_sha256"],
|
|
2185
|
+
)
|
|
2186
|
+
for match in context["matches"]:
|
|
2187
|
+
span = match["source_span"]
|
|
2188
|
+
match["match_id"] = "tpm-" + screening._sha256_text(
|
|
2189
|
+
screening._canonical_json(
|
|
2190
|
+
{
|
|
2191
|
+
"artifact_sha256": context["surface_binding"]["content_sha256"],
|
|
2192
|
+
"snapshot_sha256": context["snapshot"]["snapshot_sha256"],
|
|
2193
|
+
"surface": context["surface"],
|
|
2194
|
+
"segment_id": match["segment_id"],
|
|
2195
|
+
"context": match["context"],
|
|
2196
|
+
"rule_id": match["pattern_id"],
|
|
2197
|
+
"codepoint_start": span["codepoint_start"],
|
|
2198
|
+
"codepoint_end": span["codepoint_end"],
|
|
2199
|
+
}
|
|
2200
|
+
)
|
|
2201
|
+
)[:24]
|
|
2202
|
+
entry["bibliographic_integrity_signals"].append(duplicate)
|
|
2203
|
+
with pytest.raises(screening.ScreeningError, match="multiple current"):
|
|
2204
|
+
screening.enrich_passport(
|
|
2205
|
+
document,
|
|
2206
|
+
state=_state(),
|
|
2207
|
+
checked_at=CHECKED_AT,
|
|
2208
|
+
recorded_at=RECORDED_AT,
|
|
2209
|
+
)
|
|
2210
|
+
|
|
2211
|
+
|
|
2212
|
+
def test_internally_invalid_current_row_is_not_silently_superseded() -> None:
|
|
2213
|
+
document = _enriched()
|
|
2214
|
+
row = document["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
2215
|
+
row["signal_id"] = row["signal_id"][:-1] + (
|
|
2216
|
+
"0" if row["signal_id"][-1] != "0" else "1"
|
|
2217
|
+
)
|
|
2218
|
+
with pytest.raises(screening.ScreeningError, match="internally inconsistent"):
|
|
2219
|
+
screening.enrich_passport(
|
|
2220
|
+
document,
|
|
2221
|
+
state=_state(),
|
|
2222
|
+
checked_at=CHECKED_AT,
|
|
2223
|
+
recorded_at=RECORDED_AT,
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
|
|
2227
|
+
def test_generated_id_collision_with_preserved_legacy_row_is_rejected() -> None:
|
|
2228
|
+
document = _passport()
|
|
2229
|
+
generated = screening.build_cited_signal(
|
|
2230
|
+
document["literature_corpus"][0],
|
|
2231
|
+
surface="cited_title",
|
|
2232
|
+
state=_state(),
|
|
2233
|
+
checked_at=CHECKED_AT,
|
|
2234
|
+
recorded_at=RECORDED_AT,
|
|
2235
|
+
)
|
|
2236
|
+
legacy = json.loads(
|
|
2237
|
+
(SCRIPTS / "fixtures/bibliographic_integrity_signals/tortured_phrase.json").read_text(
|
|
2238
|
+
encoding="utf-8"
|
|
2239
|
+
)
|
|
2240
|
+
)
|
|
2241
|
+
legacy["signal_id"] = generated["signal_id"]
|
|
2242
|
+
legacy["subject"]["citation_key"] = document["literature_corpus"][0]["citation_key"]
|
|
2243
|
+
legacy["subject"]["source_pointer"] = document["literature_corpus"][0]["source_pointer"]
|
|
2244
|
+
document["literature_corpus"][0]["bibliographic_integrity_signals"] = [legacy]
|
|
2245
|
+
with pytest.raises(screening.ScreeningError, match="collides with preserved"):
|
|
2246
|
+
screening.enrich_passport(
|
|
2247
|
+
document,
|
|
2248
|
+
state=_state(),
|
|
2249
|
+
checked_at=CHECKED_AT,
|
|
2250
|
+
recorded_at=RECORDED_AT,
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
@pytest.mark.parametrize(
|
|
2255
|
+
"mutation",
|
|
2256
|
+
[
|
|
2257
|
+
"title",
|
|
2258
|
+
"source_pointer",
|
|
2259
|
+
"signal_id",
|
|
2260
|
+
"span",
|
|
2261
|
+
"count",
|
|
2262
|
+
"match_id",
|
|
2263
|
+
"evidence",
|
|
2264
|
+
],
|
|
2265
|
+
)
|
|
2266
|
+
def test_cited_binding_mutations_fail(mutation: str) -> None:
|
|
2267
|
+
output = _enriched()
|
|
2268
|
+
entry = output["literature_corpus"][0]
|
|
2269
|
+
target_surface = "cited_abstract" if mutation == "span" else "cited_title"
|
|
2270
|
+
signal = next(
|
|
2271
|
+
item
|
|
2272
|
+
for item in entry["bibliographic_integrity_signals"]
|
|
2273
|
+
if item["tortured_phrase_context"]["surface"] == target_surface
|
|
2274
|
+
)
|
|
2275
|
+
if mutation == "title":
|
|
2276
|
+
entry["title"] += " mutation"
|
|
2277
|
+
elif mutation == "source_pointer":
|
|
2278
|
+
entry["source_pointer"] += "/mutation"
|
|
2279
|
+
elif mutation == "signal_id":
|
|
2280
|
+
signal["signal_id"] = signal["signal_id"][:-1] + "0"
|
|
2281
|
+
elif mutation == "span":
|
|
2282
|
+
signal["tortured_phrase_context"]["matches"][0]["source_span"]["utf8_end"] -= 1
|
|
2283
|
+
elif mutation == "count":
|
|
2284
|
+
signal["tortured_phrase_context"]["counts"]["rule_match_count"] += 1
|
|
2285
|
+
elif mutation == "match_id":
|
|
2286
|
+
signal["tortured_phrase_context"]["matches"][0]["match_id"] = "tpm-" + "0" * 24
|
|
2287
|
+
else:
|
|
2288
|
+
signal["evidence"][0]["observed_value"] = "forged"
|
|
2289
|
+
with pytest.raises(screening.ScreeningError):
|
|
2290
|
+
screening.validate_cited_signal_binding(signal, entry)
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
@pytest.mark.parametrize(
|
|
2294
|
+
"mutation",
|
|
2295
|
+
[
|
|
2296
|
+
"missing_abstract_as_checked",
|
|
2297
|
+
"checked_with_zero_rules",
|
|
2298
|
+
"checked_with_unknown_segment",
|
|
2299
|
+
"match_context_crosses_surface",
|
|
2300
|
+
"outer_not_checked_with_loaded_snapshot",
|
|
2301
|
+
"not_checked_snapshot_keeps_hashes",
|
|
2302
|
+
"match_limit_with_not_checked_snapshot",
|
|
2303
|
+
],
|
|
2304
|
+
)
|
|
2305
|
+
def test_cited_state_and_surface_laundering_mutations_fail(mutation: str) -> None:
|
|
2306
|
+
output = _enriched()
|
|
2307
|
+
if mutation == "missing_abstract_as_checked":
|
|
2308
|
+
entry = next(
|
|
2309
|
+
item
|
|
2310
|
+
for item in output["literature_corpus"]
|
|
2311
|
+
if item["citation_key"] == "fixture_missing_2026"
|
|
2312
|
+
)
|
|
2313
|
+
signal = next(
|
|
2314
|
+
item
|
|
2315
|
+
for item in entry["bibliographic_integrity_signals"]
|
|
2316
|
+
if item["tortured_phrase_context"]["surface"] == "cited_abstract"
|
|
2317
|
+
)
|
|
2318
|
+
signal["check_status"] = "checked"
|
|
2319
|
+
signal["finding"] = "not_detected"
|
|
2320
|
+
signal["provenance"]["checked_at"] = CHECKED_AT
|
|
2321
|
+
signal["evidence"][0]["evidence_type"] = "list_record"
|
|
2322
|
+
signal["evidence"][0]["observed_value"] = 0
|
|
2323
|
+
signal["tortured_phrase_context"]["reason_code"] = "CHECK_COMPLETED"
|
|
2324
|
+
else:
|
|
2325
|
+
entry = output["literature_corpus"][0]
|
|
2326
|
+
signal = next(
|
|
2327
|
+
item
|
|
2328
|
+
for item in entry["bibliographic_integrity_signals"]
|
|
2329
|
+
if item["tortured_phrase_context"]["surface"] == "cited_title"
|
|
2330
|
+
)
|
|
2331
|
+
context = signal["tortured_phrase_context"]
|
|
2332
|
+
if mutation == "checked_with_zero_rules":
|
|
2333
|
+
context["counts"]["rules_evaluated"] = 0
|
|
2334
|
+
elif mutation == "checked_with_unknown_segment":
|
|
2335
|
+
context["counts"]["unknown_segments"] = 1
|
|
2336
|
+
elif mutation == "match_context_crosses_surface":
|
|
2337
|
+
match = context["matches"][0]
|
|
2338
|
+
match["context"] = "cited_abstract"
|
|
2339
|
+
payload = {
|
|
2340
|
+
"artifact_sha256": context["surface_binding"]["content_sha256"],
|
|
2341
|
+
"snapshot_sha256": context["snapshot"]["snapshot_sha256"],
|
|
2342
|
+
"surface": context["surface"],
|
|
2343
|
+
"segment_id": match["segment_id"],
|
|
2344
|
+
"context": match["context"],
|
|
2345
|
+
"rule_id": match["pattern_id"],
|
|
2346
|
+
"codepoint_start": match["source_span"]["codepoint_start"],
|
|
2347
|
+
"codepoint_end": match["source_span"]["codepoint_end"],
|
|
2348
|
+
}
|
|
2349
|
+
match["match_id"] = "tpm-" + screening._sha256_text(
|
|
2350
|
+
screening._canonical_json(payload)
|
|
2351
|
+
)[:24]
|
|
2352
|
+
context["counts"]["matches_by_context"]["cited_title"] -= 1
|
|
2353
|
+
context["counts"]["matches_by_context"]["cited_abstract"] += 1
|
|
2354
|
+
elif mutation in {
|
|
2355
|
+
"outer_not_checked_with_loaded_snapshot",
|
|
2356
|
+
"not_checked_snapshot_keeps_hashes",
|
|
2357
|
+
}:
|
|
2358
|
+
signal["check_status"] = "not_checked"
|
|
2359
|
+
signal["finding"] = "unresolved"
|
|
2360
|
+
signal["provenance"]["checked_at"] = None
|
|
2361
|
+
signal["evidence"][0]["evidence_type"] = "degradation_record"
|
|
2362
|
+
signal["evidence"][0]["observed_value"] = "SNAPSHOT_NOT_PROVIDED"
|
|
2363
|
+
context["reason_code"] = "SNAPSHOT_NOT_PROVIDED"
|
|
2364
|
+
context["counts"] = screening._empty_counts()
|
|
2365
|
+
context["matches"] = []
|
|
2366
|
+
if mutation == "not_checked_snapshot_keeps_hashes":
|
|
2367
|
+
context["snapshot"] = screening._snapshot_binding(
|
|
2368
|
+
screening.snapshot_state(None, None)
|
|
2369
|
+
)
|
|
2370
|
+
context["snapshot"]["snapshot_sha256"] = "0" * 64
|
|
2371
|
+
context["snapshot"]["manifest_sha256"] = "1" * 64
|
|
2372
|
+
signal["signal_id"] = screening._signal_id(
|
|
2373
|
+
entry["citation_key"],
|
|
2374
|
+
context["surface"],
|
|
2375
|
+
context["snapshot"]["snapshot_sha256"],
|
|
2376
|
+
context["surface_binding"]["content_sha256"],
|
|
2377
|
+
)
|
|
2378
|
+
signal["provenance"]["source_name"] = (
|
|
2379
|
+
"tortured-phrase snapshot unavailable"
|
|
2380
|
+
)
|
|
2381
|
+
signal["provenance"]["source_version"] = None
|
|
2382
|
+
signal["provenance"]["source_sha256"] = "0" * 64
|
|
2383
|
+
else:
|
|
2384
|
+
signal["check_status"] = "degraded"
|
|
2385
|
+
signal["finding"] = "unresolved"
|
|
2386
|
+
signal["evidence"][0]["evidence_type"] = "degradation_record"
|
|
2387
|
+
signal["evidence"][0]["observed_value"] = "MATCH_RESOURCE_LIMIT"
|
|
2388
|
+
context["reason_code"] = "MATCH_RESOURCE_LIMIT"
|
|
2389
|
+
context["counts"] = screening._empty_counts()
|
|
2390
|
+
context["matches"] = []
|
|
2391
|
+
context["snapshot"] = screening._snapshot_binding(
|
|
2392
|
+
screening.snapshot_state(None, None)
|
|
2393
|
+
)
|
|
2394
|
+
signal["provenance"]["source_name"] = (
|
|
2395
|
+
"tortured-phrase snapshot unavailable"
|
|
2396
|
+
)
|
|
2397
|
+
signal["provenance"]["source_version"] = None
|
|
2398
|
+
signal["provenance"]["source_sha256"] = None
|
|
2399
|
+
with pytest.raises(screening.ScreeningError):
|
|
2400
|
+
screening.validate_cited_signal_binding(signal, entry)
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
@pytest.mark.parametrize(
|
|
2404
|
+
"rights",
|
|
2405
|
+
[
|
|
2406
|
+
{
|
|
2407
|
+
"basis": "user_declared_authorized",
|
|
2408
|
+
"redistribution_status": "permitted",
|
|
2409
|
+
"reference": None,
|
|
2410
|
+
"user_declaration": None,
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
"basis": "written_permission",
|
|
2414
|
+
"redistribution_status": "permitted",
|
|
2415
|
+
"reference": None,
|
|
2416
|
+
"user_declaration": None,
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
"basis": "unresolved",
|
|
2420
|
+
"redistribution_status": "permitted",
|
|
2421
|
+
"reference": None,
|
|
2422
|
+
"user_declaration": None,
|
|
2423
|
+
},
|
|
2424
|
+
],
|
|
2425
|
+
)
|
|
2426
|
+
def test_cited_carrier_rejects_impossible_rights_projection(rights: dict) -> None:
|
|
2427
|
+
output = _enriched()
|
|
2428
|
+
entry = output["literature_corpus"][0]
|
|
2429
|
+
signal = entry["bibliographic_integrity_signals"][0]
|
|
2430
|
+
signal["tortured_phrase_context"]["snapshot"]["rights"] = rights
|
|
2431
|
+
with pytest.raises(screening.ScreeningError):
|
|
2432
|
+
screening.validate_cited_signal_binding(signal, entry)
|
|
2433
|
+
|
|
2434
|
+
|
|
2435
|
+
def test_cited_carrier_rejects_missing_loaded_source_locator() -> None:
|
|
2436
|
+
output = _enriched()
|
|
2437
|
+
entry = output["literature_corpus"][0]
|
|
2438
|
+
signal = entry["bibliographic_integrity_signals"][0]
|
|
2439
|
+
signal["tortured_phrase_context"]["snapshot"]["source"]["locator"] = None
|
|
2440
|
+
with pytest.raises(screening.ScreeningError):
|
|
2441
|
+
screening.validate_cited_signal_binding(signal, entry)
|
|
2442
|
+
|
|
2443
|
+
|
|
2444
|
+
def test_tortured_phrase_cannot_be_deterministic_or_terminal() -> None:
|
|
2445
|
+
schema = screening._load_schema(screening.SIGNAL_SCHEMA_PATH)
|
|
2446
|
+
validator = Draft202012Validator(
|
|
2447
|
+
schema, format_checker=Draft202012Validator.FORMAT_CHECKER
|
|
2448
|
+
)
|
|
2449
|
+
signal = _enriched()["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
2450
|
+
assert not list(validator.iter_errors(signal))
|
|
2451
|
+
signal["epistemic_class"] = "deterministic_fact"
|
|
2452
|
+
signal["epistemic_label"] = "RESOLVER-OR-LIST-OBSERVATION"
|
|
2453
|
+
assert list(validator.iter_errors(signal))
|
|
2454
|
+
signal = _enriched()["literature_corpus"][0]["bibliographic_integrity_signals"][0]
|
|
2455
|
+
signal["terminal_policy"] = {
|
|
2456
|
+
"eligible": True,
|
|
2457
|
+
"owner": "citation_finalizer",
|
|
2458
|
+
"policy_key": "tortured_phrase",
|
|
2459
|
+
"current_effect": "policy_gated",
|
|
2460
|
+
}
|
|
2461
|
+
assert list(validator.iter_errors(signal))
|
|
2462
|
+
|
|
2463
|
+
|
|
2464
|
+
def test_report_never_copies_raw_pattern_bodies() -> None:
|
|
2465
|
+
report_bytes = json.dumps(_report(), ensure_ascii=False)
|
|
2466
|
+
snapshot = json.loads((FIXTURES / "snapshot.json").read_text(encoding="utf-8"))
|
|
2467
|
+
for rule in snapshot["rules"]:
|
|
2468
|
+
expression = json.dumps(rule["expression"], ensure_ascii=False, sort_keys=True)
|
|
2469
|
+
assert expression not in report_bytes
|
|
2470
|
+
|
|
2471
|
+
|
|
2472
|
+
def test_cli_rejects_in_place_passport_output() -> None:
|
|
2473
|
+
result = subprocess.run(
|
|
2474
|
+
[
|
|
2475
|
+
sys.executable,
|
|
2476
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2477
|
+
"enrich-passport",
|
|
2478
|
+
"--input",
|
|
2479
|
+
str(FIXTURES / "corpus_input.yaml"),
|
|
2480
|
+
"--output",
|
|
2481
|
+
str(FIXTURES / "corpus_input.yaml"),
|
|
2482
|
+
"--snapshot",
|
|
2483
|
+
str(FIXTURES / "snapshot.json"),
|
|
2484
|
+
"--snapshot-manifest",
|
|
2485
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2486
|
+
"--checked-at",
|
|
2487
|
+
CHECKED_AT,
|
|
2488
|
+
"--recorded-at",
|
|
2489
|
+
RECORDED_AT,
|
|
2490
|
+
],
|
|
2491
|
+
text=True,
|
|
2492
|
+
capture_output=True,
|
|
2493
|
+
check=False,
|
|
2494
|
+
)
|
|
2495
|
+
assert result.returncode == 1
|
|
2496
|
+
assert "refuses in-place" in result.stderr
|
|
2497
|
+
|
|
2498
|
+
|
|
2499
|
+
@pytest.mark.parametrize("alias", ["snapshot", "manifest", "snapshot_hardlink"])
|
|
2500
|
+
def test_scan_output_cannot_alias_any_named_input(
|
|
2501
|
+
tmp_path: Path, alias: str
|
|
2502
|
+
) -> None:
|
|
2503
|
+
draft = tmp_path / "draft.md"
|
|
2504
|
+
snapshot = tmp_path / "snapshot.json"
|
|
2505
|
+
manifest = tmp_path / "manifest.json"
|
|
2506
|
+
draft.write_bytes((FIXTURES / "own_draft.md").read_bytes())
|
|
2507
|
+
snapshot.write_bytes((FIXTURES / "snapshot.json").read_bytes())
|
|
2508
|
+
manifest.write_bytes((FIXTURES / "snapshot_manifest.json").read_bytes())
|
|
2509
|
+
if alias == "snapshot":
|
|
2510
|
+
output = snapshot
|
|
2511
|
+
elif alias == "manifest":
|
|
2512
|
+
output = manifest
|
|
2513
|
+
else:
|
|
2514
|
+
output = tmp_path / "snapshot-hardlink.json"
|
|
2515
|
+
output.hardlink_to(snapshot)
|
|
2516
|
+
snapshot_before = snapshot.read_bytes()
|
|
2517
|
+
manifest_before = manifest.read_bytes()
|
|
2518
|
+
result = subprocess.run(
|
|
2519
|
+
[
|
|
2520
|
+
sys.executable,
|
|
2521
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2522
|
+
"scan-draft",
|
|
2523
|
+
"--input",
|
|
2524
|
+
str(draft),
|
|
2525
|
+
"--artifact-id",
|
|
2526
|
+
"draft.md",
|
|
2527
|
+
"--format",
|
|
2528
|
+
"markdown",
|
|
2529
|
+
"--snapshot",
|
|
2530
|
+
str(snapshot),
|
|
2531
|
+
"--snapshot-manifest",
|
|
2532
|
+
str(manifest),
|
|
2533
|
+
"--checked-at",
|
|
2534
|
+
CHECKED_AT,
|
|
2535
|
+
"--recorded-at",
|
|
2536
|
+
RECORDED_AT,
|
|
2537
|
+
"--output",
|
|
2538
|
+
str(output),
|
|
2539
|
+
],
|
|
2540
|
+
text=True,
|
|
2541
|
+
capture_output=True,
|
|
2542
|
+
check=False,
|
|
2543
|
+
)
|
|
2544
|
+
assert result.returncode == 1
|
|
2545
|
+
assert "named-input alias" in result.stderr
|
|
2546
|
+
assert snapshot.read_bytes() == snapshot_before
|
|
2547
|
+
assert manifest.read_bytes() == manifest_before
|
|
2548
|
+
|
|
2549
|
+
|
|
2550
|
+
def test_corpus_output_match_cap_rejects_before_write(
|
|
2551
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2552
|
+
) -> None:
|
|
2553
|
+
monkeypatch.setattr(screening, "MAX_CORPUS_OUTPUT_MATCHES", 0)
|
|
2554
|
+
with pytest.raises(screening.MatchLimitError, match="corpus output match count"):
|
|
2555
|
+
_enriched()
|
|
2556
|
+
|
|
2557
|
+
|
|
2558
|
+
@pytest.mark.parametrize(
|
|
2559
|
+
("limit_name", "document", "message"),
|
|
2560
|
+
[
|
|
2561
|
+
(
|
|
2562
|
+
"MAX_CORPUS_ENTRIES",
|
|
2563
|
+
{"literature_corpus": [{}, {}]},
|
|
2564
|
+
"more than 1 entries",
|
|
2565
|
+
),
|
|
2566
|
+
(
|
|
2567
|
+
"MAX_CORPUS_EXISTING_SIGNALS",
|
|
2568
|
+
{"literature_corpus": [{"bibliographic_integrity_signals": [{}]}]},
|
|
2569
|
+
"exceed 0 rows",
|
|
2570
|
+
),
|
|
2571
|
+
],
|
|
2572
|
+
)
|
|
2573
|
+
def test_corpus_cardinality_caps_fail_before_copy_or_row_construction(
|
|
2574
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2575
|
+
limit_name: str,
|
|
2576
|
+
document: dict,
|
|
2577
|
+
message: str,
|
|
2578
|
+
) -> None:
|
|
2579
|
+
monkeypatch.setattr(screening, limit_name, 1 if limit_name.endswith("ENTRIES") else 0)
|
|
2580
|
+
|
|
2581
|
+
def unexpected_copy(_value):
|
|
2582
|
+
raise AssertionError("copy must not run after an over-limit input")
|
|
2583
|
+
|
|
2584
|
+
monkeypatch.setattr(screening.copy, "deepcopy", unexpected_copy)
|
|
2585
|
+
with pytest.raises(screening.MatchLimitError, match=message):
|
|
2586
|
+
screening.enrich_passport(
|
|
2587
|
+
document,
|
|
2588
|
+
state=screening.snapshot_state(None, None),
|
|
2589
|
+
checked_at=CHECKED_AT,
|
|
2590
|
+
recorded_at=RECORDED_AT,
|
|
2591
|
+
)
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
def test_deep_json_passport_fails_closed_without_traceback_or_output_mutation(
|
|
2595
|
+
tmp_path: Path,
|
|
2596
|
+
) -> None:
|
|
2597
|
+
passport = tmp_path / "deep-passport.json"
|
|
2598
|
+
passport.write_text(
|
|
2599
|
+
'{"literature_corpus":[],"extension":'
|
|
2600
|
+
+ "[" * 1000
|
|
2601
|
+
+ "0"
|
|
2602
|
+
+ "]" * 1000
|
|
2603
|
+
+ "}\n",
|
|
2604
|
+
encoding="utf-8",
|
|
2605
|
+
)
|
|
2606
|
+
output = tmp_path / "output.json"
|
|
2607
|
+
output.write_bytes(b"sentinel\n")
|
|
2608
|
+
result = subprocess.run(
|
|
2609
|
+
[
|
|
2610
|
+
sys.executable,
|
|
2611
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2612
|
+
"enrich-passport",
|
|
2613
|
+
"--input",
|
|
2614
|
+
str(passport),
|
|
2615
|
+
"--output",
|
|
2616
|
+
str(output),
|
|
2617
|
+
"--checked-at",
|
|
2618
|
+
CHECKED_AT,
|
|
2619
|
+
"--recorded-at",
|
|
2620
|
+
RECORDED_AT,
|
|
2621
|
+
],
|
|
2622
|
+
text=True,
|
|
2623
|
+
capture_output=True,
|
|
2624
|
+
check=False,
|
|
2625
|
+
)
|
|
2626
|
+
assert result.returncode == 1
|
|
2627
|
+
assert "Traceback" not in result.stderr
|
|
2628
|
+
assert "strict JSON" in result.stderr or "structure exceeds" in result.stderr
|
|
2629
|
+
assert output.read_bytes() == b"sentinel\n"
|
|
2630
|
+
|
|
2631
|
+
|
|
2632
|
+
def test_deep_advisory_report_fails_closed_without_traceback(tmp_path: Path) -> None:
|
|
2633
|
+
report = tmp_path / "deep-report.json"
|
|
2634
|
+
report.write_text(
|
|
2635
|
+
'{"extension":' + "[" * 1000 + "0" + "]" * 1000 + "}\n",
|
|
2636
|
+
encoding="utf-8",
|
|
2637
|
+
)
|
|
2638
|
+
result = subprocess.run(
|
|
2639
|
+
[
|
|
2640
|
+
sys.executable,
|
|
2641
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2642
|
+
"validate-draft",
|
|
2643
|
+
"--input",
|
|
2644
|
+
str(FIXTURES / "own_draft.md"),
|
|
2645
|
+
"--artifact-id",
|
|
2646
|
+
"own_draft.md",
|
|
2647
|
+
"--format",
|
|
2648
|
+
"markdown",
|
|
2649
|
+
"--snapshot",
|
|
2650
|
+
str(FIXTURES / "snapshot.json"),
|
|
2651
|
+
"--snapshot-manifest",
|
|
2652
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2653
|
+
"--report",
|
|
2654
|
+
str(report),
|
|
2655
|
+
],
|
|
2656
|
+
text=True,
|
|
2657
|
+
capture_output=True,
|
|
2658
|
+
check=False,
|
|
2659
|
+
)
|
|
2660
|
+
assert result.returncode == 1
|
|
2661
|
+
assert "Traceback" not in result.stderr
|
|
2662
|
+
assert "strict JSON" in result.stderr or "structure exceeds" in result.stderr
|
|
2663
|
+
|
|
2664
|
+
|
|
2665
|
+
@pytest.mark.parametrize("cwd", [REPO_ROOT, SCRIPTS])
|
|
2666
|
+
def test_runtime_imports_in_package_and_direct_script_modes(cwd: Path) -> None:
|
|
2667
|
+
module = "scripts.tortured_phrase_screening" if cwd == REPO_ROOT else "tortured_phrase_screening"
|
|
2668
|
+
result = subprocess.run(
|
|
2669
|
+
[sys.executable, "-c", f"import {module}"],
|
|
2670
|
+
cwd=cwd,
|
|
2671
|
+
text=True,
|
|
2672
|
+
capture_output=True,
|
|
2673
|
+
check=False,
|
|
2674
|
+
)
|
|
2675
|
+
assert result.returncode == 0, result.stderr
|
|
2676
|
+
|
|
2677
|
+
|
|
2678
|
+
def test_package_import_cannot_bind_shadow_bibliographic_module(
|
|
2679
|
+
tmp_path: Path,
|
|
2680
|
+
) -> None:
|
|
2681
|
+
(tmp_path / "bibliographic_integrity_signals.py").write_text(
|
|
2682
|
+
"raise RuntimeError('shadow module imported')\n", encoding="utf-8"
|
|
2683
|
+
)
|
|
2684
|
+
code = (
|
|
2685
|
+
"import sys; "
|
|
2686
|
+
f"sys.path.insert(0, {str(tmp_path)!r}); "
|
|
2687
|
+
"import scripts.tortured_phrase_screening as module; "
|
|
2688
|
+
"print(module._validate_existing_phrase_projection.__module__); "
|
|
2689
|
+
"print(module._validate_existing_phrase_projection.__code__.co_filename)"
|
|
2690
|
+
)
|
|
2691
|
+
result = subprocess.run(
|
|
2692
|
+
[sys.executable, "-c", code],
|
|
2693
|
+
cwd=REPO_ROOT,
|
|
2694
|
+
text=True,
|
|
2695
|
+
capture_output=True,
|
|
2696
|
+
check=False,
|
|
2697
|
+
)
|
|
2698
|
+
assert result.returncode == 0, result.stderr
|
|
2699
|
+
lines = result.stdout.splitlines()
|
|
2700
|
+
assert lines[0] == "scripts.bibliographic_integrity_signals"
|
|
2701
|
+
assert Path(lines[1]).resolve() == (
|
|
2702
|
+
SCRIPTS / "bibliographic_integrity_signals.py"
|
|
2703
|
+
).resolve()
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
def test_yaml_aliases_fail_closed_without_output(tmp_path: Path) -> None:
|
|
2707
|
+
passport = tmp_path / "aliased.yaml"
|
|
2708
|
+
passport.write_text(
|
|
2709
|
+
"shared: &rows []\nliterature_corpus: *rows\n",
|
|
2710
|
+
encoding="utf-8",
|
|
2711
|
+
)
|
|
2712
|
+
output = tmp_path / "output.yaml"
|
|
2713
|
+
result = subprocess.run(
|
|
2714
|
+
[
|
|
2715
|
+
sys.executable,
|
|
2716
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2717
|
+
"enrich-passport",
|
|
2718
|
+
"--input",
|
|
2719
|
+
str(passport),
|
|
2720
|
+
"--output",
|
|
2721
|
+
str(output),
|
|
2722
|
+
"--snapshot",
|
|
2723
|
+
str(FIXTURES / "snapshot.json"),
|
|
2724
|
+
"--snapshot-manifest",
|
|
2725
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2726
|
+
"--checked-at",
|
|
2727
|
+
CHECKED_AT,
|
|
2728
|
+
"--recorded-at",
|
|
2729
|
+
RECORDED_AT,
|
|
2730
|
+
],
|
|
2731
|
+
text=True,
|
|
2732
|
+
capture_output=True,
|
|
2733
|
+
check=False,
|
|
2734
|
+
)
|
|
2735
|
+
assert result.returncode == 1
|
|
2736
|
+
assert "YAML aliases are forbidden" in result.stderr
|
|
2737
|
+
assert not output.exists()
|
|
2738
|
+
|
|
2739
|
+
|
|
2740
|
+
def test_cli_has_no_all_or_page_traversal() -> None:
|
|
2741
|
+
result = subprocess.run(
|
|
2742
|
+
[
|
|
2743
|
+
sys.executable,
|
|
2744
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2745
|
+
"render-draft",
|
|
2746
|
+
"--help",
|
|
2747
|
+
],
|
|
2748
|
+
text=True,
|
|
2749
|
+
capture_output=True,
|
|
2750
|
+
check=False,
|
|
2751
|
+
)
|
|
2752
|
+
assert result.returncode == 0
|
|
2753
|
+
assert "--all" not in result.stdout
|
|
2754
|
+
assert "--page" not in result.stdout
|
|
2755
|
+
|
|
2756
|
+
|
|
2757
|
+
def test_invalid_snapshot_cli_writes_degraded_artifact_and_returns_one(tmp_path: Path) -> None:
|
|
2758
|
+
bad = tmp_path / "bad.json"
|
|
2759
|
+
bad.write_text("{}\n", encoding="utf-8")
|
|
2760
|
+
output = tmp_path / "report.json"
|
|
2761
|
+
result = subprocess.run(
|
|
2762
|
+
[
|
|
2763
|
+
sys.executable,
|
|
2764
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2765
|
+
"scan-draft",
|
|
2766
|
+
"--input",
|
|
2767
|
+
str(FIXTURES / "own_draft.md"),
|
|
2768
|
+
"--artifact-id",
|
|
2769
|
+
"own_draft.md",
|
|
2770
|
+
"--format",
|
|
2771
|
+
"markdown",
|
|
2772
|
+
"--snapshot",
|
|
2773
|
+
str(bad),
|
|
2774
|
+
"--snapshot-manifest",
|
|
2775
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2776
|
+
"--checked-at",
|
|
2777
|
+
CHECKED_AT,
|
|
2778
|
+
"--recorded-at",
|
|
2779
|
+
RECORDED_AT,
|
|
2780
|
+
"--output",
|
|
2781
|
+
str(output),
|
|
2782
|
+
],
|
|
2783
|
+
text=True,
|
|
2784
|
+
capture_output=True,
|
|
2785
|
+
check=False,
|
|
2786
|
+
)
|
|
2787
|
+
assert result.returncode == 1
|
|
2788
|
+
report = json.loads(output.read_text(encoding="utf-8"))
|
|
2789
|
+
assert report["check_status"] == "degraded"
|
|
2790
|
+
assert report["finding"] == "unresolved"
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
@pytest.mark.parametrize(
|
|
2794
|
+
("text", "reason"),
|
|
2795
|
+
[
|
|
2796
|
+
("", "DOCUMENT_EMPTY"),
|
|
2797
|
+
("`unclosed luminous turnip", "DOCUMENT_PARSE_DEGRADED"),
|
|
2798
|
+
],
|
|
2799
|
+
)
|
|
2800
|
+
def test_loaded_snapshot_scan_returns_one_for_degraded_artifact(
|
|
2801
|
+
tmp_path: Path, text: str, reason: str
|
|
2802
|
+
) -> None:
|
|
2803
|
+
draft = tmp_path / "draft.md"
|
|
2804
|
+
draft.write_text(text, encoding="utf-8")
|
|
2805
|
+
output = tmp_path / "report.json"
|
|
2806
|
+
result = subprocess.run(
|
|
2807
|
+
[
|
|
2808
|
+
sys.executable,
|
|
2809
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2810
|
+
"scan-draft",
|
|
2811
|
+
"--input",
|
|
2812
|
+
str(draft),
|
|
2813
|
+
"--artifact-id",
|
|
2814
|
+
"draft.md",
|
|
2815
|
+
"--format",
|
|
2816
|
+
"markdown",
|
|
2817
|
+
"--snapshot",
|
|
2818
|
+
str(FIXTURES / "snapshot.json"),
|
|
2819
|
+
"--snapshot-manifest",
|
|
2820
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2821
|
+
"--checked-at",
|
|
2822
|
+
CHECKED_AT,
|
|
2823
|
+
"--recorded-at",
|
|
2824
|
+
RECORDED_AT,
|
|
2825
|
+
"--output",
|
|
2826
|
+
str(output),
|
|
2827
|
+
],
|
|
2828
|
+
text=True,
|
|
2829
|
+
capture_output=True,
|
|
2830
|
+
check=False,
|
|
2831
|
+
)
|
|
2832
|
+
assert result.returncode == 1
|
|
2833
|
+
report = json.loads(output.read_text(encoding="utf-8"))
|
|
2834
|
+
assert report["check_status"] == "degraded"
|
|
2835
|
+
assert report["reason_code"] == reason
|
|
2836
|
+
|
|
2837
|
+
|
|
2838
|
+
def test_loaded_snapshot_resource_degradation_returns_one_after_write(
|
|
2839
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
2840
|
+
) -> None:
|
|
2841
|
+
draft = tmp_path / "draft.md"
|
|
2842
|
+
draft.write_text("alpha\n\nbeta", encoding="utf-8")
|
|
2843
|
+
output = tmp_path / "report.json"
|
|
2844
|
+
monkeypatch.setattr(screening, "MAX_SEGMENTS", 1)
|
|
2845
|
+
result = screening.main(
|
|
2846
|
+
[
|
|
2847
|
+
"scan-draft",
|
|
2848
|
+
"--input",
|
|
2849
|
+
str(draft),
|
|
2850
|
+
"--artifact-id",
|
|
2851
|
+
"draft.md",
|
|
2852
|
+
"--format",
|
|
2853
|
+
"markdown",
|
|
2854
|
+
"--snapshot",
|
|
2855
|
+
str(FIXTURES / "snapshot.json"),
|
|
2856
|
+
"--snapshot-manifest",
|
|
2857
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2858
|
+
"--checked-at",
|
|
2859
|
+
CHECKED_AT,
|
|
2860
|
+
"--recorded-at",
|
|
2861
|
+
RECORDED_AT,
|
|
2862
|
+
"--output",
|
|
2863
|
+
str(output),
|
|
2864
|
+
]
|
|
2865
|
+
)
|
|
2866
|
+
assert result == 1
|
|
2867
|
+
report = json.loads(output.read_text(encoding="utf-8"))
|
|
2868
|
+
assert report["check_status"] == "degraded"
|
|
2869
|
+
assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
|
|
2870
|
+
|
|
2871
|
+
|
|
2872
|
+
def test_loaded_snapshot_corpus_degradation_returns_one_after_write(
|
|
2873
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
2874
|
+
) -> None:
|
|
2875
|
+
passport = tmp_path / "corpus.yaml"
|
|
2876
|
+
passport.write_bytes((FIXTURES / "corpus_input.yaml").read_bytes())
|
|
2877
|
+
output = tmp_path / "output.yaml"
|
|
2878
|
+
monkeypatch.setattr(screening, "MAX_MATCH_WORK_UNITS", 0)
|
|
2879
|
+
result = screening.main(
|
|
2880
|
+
[
|
|
2881
|
+
"enrich-passport",
|
|
2882
|
+
"--input",
|
|
2883
|
+
str(passport),
|
|
2884
|
+
"--output",
|
|
2885
|
+
str(output),
|
|
2886
|
+
"--snapshot",
|
|
2887
|
+
str(FIXTURES / "snapshot.json"),
|
|
2888
|
+
"--snapshot-manifest",
|
|
2889
|
+
str(FIXTURES / "snapshot_manifest.json"),
|
|
2890
|
+
"--checked-at",
|
|
2891
|
+
CHECKED_AT,
|
|
2892
|
+
"--recorded-at",
|
|
2893
|
+
RECORDED_AT,
|
|
2894
|
+
]
|
|
2895
|
+
)
|
|
2896
|
+
assert result == 1
|
|
2897
|
+
enriched, _kind = screening._load_passport(output)
|
|
2898
|
+
assert any(
|
|
2899
|
+
signal.get("check_status") == "degraded"
|
|
2900
|
+
for entry in enriched["literature_corpus"]
|
|
2901
|
+
for signal in entry["bibliographic_integrity_signals"]
|
|
2902
|
+
if signal.get("schema_version") == screening.SIGNAL_VERSION
|
|
2903
|
+
)
|
|
2904
|
+
|
|
2905
|
+
|
|
2906
|
+
def test_explicit_not_checked_scan_remains_successful(tmp_path: Path) -> None:
|
|
2907
|
+
output = tmp_path / "report.json"
|
|
2908
|
+
result = screening.main(
|
|
2909
|
+
[
|
|
2910
|
+
"scan-draft",
|
|
2911
|
+
"--input",
|
|
2912
|
+
str(FIXTURES / "own_draft.md"),
|
|
2913
|
+
"--artifact-id",
|
|
2914
|
+
"own_draft.md",
|
|
2915
|
+
"--format",
|
|
2916
|
+
"markdown",
|
|
2917
|
+
"--checked-at",
|
|
2918
|
+
CHECKED_AT,
|
|
2919
|
+
"--recorded-at",
|
|
2920
|
+
RECORDED_AT,
|
|
2921
|
+
"--output",
|
|
2922
|
+
str(output),
|
|
2923
|
+
]
|
|
2924
|
+
)
|
|
2925
|
+
assert result == 0
|
|
2926
|
+
assert json.loads(output.read_text(encoding="utf-8"))["check_status"] == (
|
|
2927
|
+
"not_checked"
|
|
2928
|
+
)
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
def test_failed_scan_does_not_overwrite_input_or_partial_output(tmp_path: Path) -> None:
|
|
2932
|
+
draft = tmp_path / "draft.md"
|
|
2933
|
+
draft.write_text("luminous turnip", encoding="utf-8")
|
|
2934
|
+
before = draft.read_bytes()
|
|
2935
|
+
result = subprocess.run(
|
|
2936
|
+
[
|
|
2937
|
+
sys.executable,
|
|
2938
|
+
str(SCRIPTS / "tortured_phrase_screening.py"),
|
|
2939
|
+
"scan-draft",
|
|
2940
|
+
"--input",
|
|
2941
|
+
str(draft),
|
|
2942
|
+
"--artifact-id",
|
|
2943
|
+
"draft.md",
|
|
2944
|
+
"--format",
|
|
2945
|
+
"markdown",
|
|
2946
|
+
"--checked-at",
|
|
2947
|
+
"bad-time",
|
|
2948
|
+
"--recorded-at",
|
|
2949
|
+
RECORDED_AT,
|
|
2950
|
+
"--output",
|
|
2951
|
+
str(tmp_path / "report.json"),
|
|
2952
|
+
],
|
|
2953
|
+
text=True,
|
|
2954
|
+
capture_output=True,
|
|
2955
|
+
check=False,
|
|
2956
|
+
)
|
|
2957
|
+
assert result.returncode == 1
|
|
2958
|
+
assert draft.read_bytes() == before
|
|
2959
|
+
assert not (tmp_path / "report.json").exists()
|