eduevidence 5.2.0
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/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- package/visualization/eduevidence-report/themes/presentation.css +140 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Immutable revision snapshots with atomic HEAD commits.
|
|
2
|
+
|
|
3
|
+
A commit either fully lands (revision dir + HEAD + project mirror) or leaves
|
|
4
|
+
the prior active revision intact. A crash after the immutable revision
|
|
5
|
+
directory exists but before HEAD changes leaves only an inactive orphan
|
|
6
|
+
revision, which readers ignore. `graph/HEAD` is authoritative; the
|
|
7
|
+
`project.json.graph_revision` mirror is repaired/flagged on divergence.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from engine.contracts import validate_record
|
|
20
|
+
from engine.graph_validate import validate_graph
|
|
21
|
+
from engine.project import ProjectWorkspace
|
|
22
|
+
|
|
23
|
+
GRAPH_TABLES = (
|
|
24
|
+
"sources", "studies", "findings", "outcomes",
|
|
25
|
+
"claims", "evidence_links", "audits",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# graph table name -> entity id field
|
|
29
|
+
_TABLE_ID_KEY = {
|
|
30
|
+
"sources": "source_id",
|
|
31
|
+
"studies": "study_id",
|
|
32
|
+
"findings": "finding_id",
|
|
33
|
+
"outcomes": "outcome_id",
|
|
34
|
+
"claims": "claim_id",
|
|
35
|
+
"evidence_links": "evidence_link_id",
|
|
36
|
+
"audits": "audit_id",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# graph table name -> schema name
|
|
40
|
+
_TABLE_SCHEMA = {
|
|
41
|
+
"sources": "source",
|
|
42
|
+
"studies": "study",
|
|
43
|
+
"findings": "finding",
|
|
44
|
+
"outcomes": "outcome",
|
|
45
|
+
"claims": "claim",
|
|
46
|
+
"evidence_links": "evidence-link",
|
|
47
|
+
"audits": "methodology-audit",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_REV_FORMAT = "rev-{:06d}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _now_iso() -> str:
|
|
54
|
+
return datetime.now(timezone.utc).isoformat()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class GraphMutation:
|
|
59
|
+
upserts: dict[str, list[dict]] = field(default_factory=dict)
|
|
60
|
+
retire_ids: dict[str, list[str]] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class GraphRevision:
|
|
65
|
+
revision: int
|
|
66
|
+
parent_revision: int
|
|
67
|
+
run_id: str
|
|
68
|
+
reason: str
|
|
69
|
+
touched_entities: dict[str, list[str]]
|
|
70
|
+
before_hash: str
|
|
71
|
+
after_hash: str
|
|
72
|
+
created_at: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _table_hash(rows: list[dict], id_key: str) -> str:
|
|
76
|
+
"""Deterministic canonical hash of one table (sorted by entity id)."""
|
|
77
|
+
ordered = sorted(rows, key=lambda r: r[id_key])
|
|
78
|
+
h = hashlib.sha256()
|
|
79
|
+
for row in ordered:
|
|
80
|
+
h.update(json.dumps(row, sort_keys=True, separators=(",", ":")).encode("utf-8"))
|
|
81
|
+
h.update(b"\n")
|
|
82
|
+
return h.hexdigest()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class GraphStore:
|
|
86
|
+
"""Versioned evidence graph for one Project."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, project: ProjectWorkspace):
|
|
89
|
+
self.project = project
|
|
90
|
+
self.graph_dir = project.path / "graph"
|
|
91
|
+
self.revisions_dir = self.graph_dir / "revisions"
|
|
92
|
+
self.head_path = self.graph_dir / "HEAD"
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def create(cls, project: ProjectWorkspace) -> "GraphStore":
|
|
96
|
+
store = cls(project)
|
|
97
|
+
store.revisions_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
if not store.head_path.is_file():
|
|
99
|
+
_atomic_write_text(store.head_path, "0")
|
|
100
|
+
return store
|
|
101
|
+
|
|
102
|
+
# ---- reads -----------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def active_revision(self) -> int:
|
|
105
|
+
return int(self.head_path.read_text(encoding="utf-8").strip())
|
|
106
|
+
|
|
107
|
+
def _revision_dir(self, revision: int) -> Path:
|
|
108
|
+
return self.revisions_dir / _REV_FORMAT.format(revision)
|
|
109
|
+
|
|
110
|
+
def _read_snapshot(self, revision: int) -> dict[str, list[dict]]:
|
|
111
|
+
"""Read the complete active snapshot; empty tables for revision 0."""
|
|
112
|
+
if revision == 0:
|
|
113
|
+
return {table: [] for table in GRAPH_TABLES}
|
|
114
|
+
rev_dir = self._revision_dir(revision)
|
|
115
|
+
snapshot: dict[str, list[dict]] = {}
|
|
116
|
+
for table in GRAPH_TABLES:
|
|
117
|
+
path = rev_dir / f"{table}.jsonl"
|
|
118
|
+
if not path.is_file():
|
|
119
|
+
raise FileNotFoundError(
|
|
120
|
+
f"revision {revision} is missing table file {path.name}; "
|
|
121
|
+
f"graph is corrupt"
|
|
122
|
+
)
|
|
123
|
+
rows = []
|
|
124
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
125
|
+
if line.strip():
|
|
126
|
+
rows.append(json.loads(line))
|
|
127
|
+
snapshot[table] = rows
|
|
128
|
+
return snapshot
|
|
129
|
+
|
|
130
|
+
def read_table(self, table: str) -> list[dict]:
|
|
131
|
+
if table not in GRAPH_TABLES:
|
|
132
|
+
raise ValueError(f"unknown graph table {table!r}")
|
|
133
|
+
return self._read_snapshot(self.active_revision())[table]
|
|
134
|
+
|
|
135
|
+
def get(self, table: str, entity_id: str) -> dict | None:
|
|
136
|
+
id_key = _TABLE_ID_KEY[table]
|
|
137
|
+
for row in self.read_table(table):
|
|
138
|
+
if row[id_key] == entity_id:
|
|
139
|
+
return row
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def canonical_hash(self, revision: int | None = None) -> str:
|
|
143
|
+
"""Deterministic canonical hash of a snapshot (default: active)."""
|
|
144
|
+
snapshot = self._read_snapshot(
|
|
145
|
+
self.active_revision() if revision is None else revision
|
|
146
|
+
)
|
|
147
|
+
return self._hash_snapshot(snapshot)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _hash_snapshot(snapshot: dict[str, list[dict]]) -> str:
|
|
151
|
+
h = hashlib.sha256()
|
|
152
|
+
for table in GRAPH_TABLES:
|
|
153
|
+
h.update(_table_hash(snapshot.get(table, []), _TABLE_ID_KEY[table]).encode("utf-8"))
|
|
154
|
+
h.update(b"\n")
|
|
155
|
+
return h.hexdigest()
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def empty_graph_hash(cls) -> str:
|
|
159
|
+
return cls._hash_snapshot({t: [] for t in GRAPH_TABLES})
|
|
160
|
+
|
|
161
|
+
# ---- validation ------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def validate(self) -> list[str]:
|
|
164
|
+
"""Cross-entity integrity + HEAD/mirror consistency of active state."""
|
|
165
|
+
problems: list[str] = []
|
|
166
|
+
try:
|
|
167
|
+
snapshot = self._read_snapshot(self.active_revision())
|
|
168
|
+
except FileNotFoundError as exc:
|
|
169
|
+
return [str(exc)]
|
|
170
|
+
problems.extend(validate_graph(snapshot))
|
|
171
|
+
mirror = self.project.current_revision()
|
|
172
|
+
head = self.active_revision()
|
|
173
|
+
if mirror != head:
|
|
174
|
+
problems.append(
|
|
175
|
+
f"project.json graph_revision ({mirror}) diverges from "
|
|
176
|
+
f"graph/HEAD ({head}); repair with repair_head_mirror()"
|
|
177
|
+
)
|
|
178
|
+
return problems
|
|
179
|
+
|
|
180
|
+
def repair_head_mirror(self) -> None:
|
|
181
|
+
"""HEAD is authoritative; mirror the project manifest to it."""
|
|
182
|
+
self.project.update_manifest(graph_revision=self.active_revision())
|
|
183
|
+
|
|
184
|
+
# ---- commit ----------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def commit(self, *, run_id: str, reason: str,
|
|
187
|
+
mutation: GraphMutation) -> GraphRevision:
|
|
188
|
+
before_rev = self.active_revision()
|
|
189
|
+
before_snapshot = self._read_snapshot(before_rev)
|
|
190
|
+
next_rev = before_rev + 1
|
|
191
|
+
|
|
192
|
+
# 1. apply mutation in memory
|
|
193
|
+
after_snapshot = self._apply(before_snapshot, mutation)
|
|
194
|
+
|
|
195
|
+
# 2. per-entity schema validation
|
|
196
|
+
for table, rows in after_snapshot.items():
|
|
197
|
+
schema = _TABLE_SCHEMA[table]
|
|
198
|
+
id_key = _TABLE_ID_KEY[table]
|
|
199
|
+
for row in rows:
|
|
200
|
+
errors = validate_record(schema, row)
|
|
201
|
+
if errors:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"commit rejected: {table} {row.get(id_key, '?')} "
|
|
204
|
+
f"fails schema: {'; '.join(errors)}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# 3. cross-entity validation
|
|
208
|
+
problems = validate_graph(after_snapshot)
|
|
209
|
+
if problems:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"commit rejected: cross-entity integrity:\n- " + "\n- ".join(problems)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
before_hash = self._hash_snapshot(before_snapshot)
|
|
215
|
+
after_hash = self._hash_snapshot(after_snapshot)
|
|
216
|
+
|
|
217
|
+
# 4. write the complete next snapshot to a temp revision dir
|
|
218
|
+
tmp_dir = self.revisions_dir / f".tmp-{run_id}"
|
|
219
|
+
if tmp_dir.exists():
|
|
220
|
+
import shutil
|
|
221
|
+
shutil.rmtree(tmp_dir)
|
|
222
|
+
tmp_dir.mkdir(parents=True)
|
|
223
|
+
try:
|
|
224
|
+
for table in GRAPH_TABLES:
|
|
225
|
+
_write_jsonl(tmp_dir / f"{table}.jsonl", after_snapshot[table])
|
|
226
|
+
touched = {
|
|
227
|
+
table: [row[_TABLE_ID_KEY[table]] for row in mutation.upserts.get(table, [])]
|
|
228
|
+
+ list(mutation.retire_ids.get(table, []))
|
|
229
|
+
for table in GRAPH_TABLES
|
|
230
|
+
if mutation.upserts.get(table) or mutation.retire_ids.get(table)
|
|
231
|
+
}
|
|
232
|
+
manifest = {
|
|
233
|
+
"revision": next_rev,
|
|
234
|
+
"parent_revision": before_rev,
|
|
235
|
+
"run_id": run_id,
|
|
236
|
+
"reason": reason,
|
|
237
|
+
"touched_entities": touched,
|
|
238
|
+
"before_hash": before_hash,
|
|
239
|
+
"after_hash": after_hash,
|
|
240
|
+
"created_at": _now_iso(),
|
|
241
|
+
"extensions": {},
|
|
242
|
+
}
|
|
243
|
+
manifest_errors = validate_record("graph-revision", manifest)
|
|
244
|
+
if manifest_errors:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"commit rejected: revision manifest invalid: "
|
|
247
|
+
f"{'; '.join(manifest_errors)}")
|
|
248
|
+
_write_json(tmp_dir / "manifest.json", manifest)
|
|
249
|
+
|
|
250
|
+
# 5. atomically promote: .tmp-<run> -> rev-00000N
|
|
251
|
+
rev_dir = self._revision_dir(next_rev)
|
|
252
|
+
if rev_dir.exists():
|
|
253
|
+
# an orphan occupying the next revision number (not reached
|
|
254
|
+
# by HEAD) is inactive: remove it and retake the number
|
|
255
|
+
if self.active_revision() == before_rev:
|
|
256
|
+
import shutil
|
|
257
|
+
shutil.rmtree(rev_dir)
|
|
258
|
+
else:
|
|
259
|
+
raise FileExistsError(
|
|
260
|
+
f"refusing to rewrite existing revision directory {rev_dir}"
|
|
261
|
+
)
|
|
262
|
+
os.replace(tmp_dir, rev_dir)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# 6. atomically switch HEAD
|
|
266
|
+
_atomic_write_text(self.head_path, str(next_rev))
|
|
267
|
+
|
|
268
|
+
# 7. atomically mirror into project.json; on failure roll HEAD
|
|
269
|
+
# back so the caller never sees a failure for a landed commit
|
|
270
|
+
try:
|
|
271
|
+
self.project.update_manifest(graph_revision=next_rev)
|
|
272
|
+
except Exception:
|
|
273
|
+
_atomic_write_text(self.head_path, str(before_rev))
|
|
274
|
+
raise
|
|
275
|
+
except Exception:
|
|
276
|
+
# never leave a half-promoted state behind
|
|
277
|
+
if tmp_dir.exists():
|
|
278
|
+
import shutil
|
|
279
|
+
shutil.rmtree(tmp_dir)
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
return GraphRevision(
|
|
283
|
+
revision=next_rev,
|
|
284
|
+
parent_revision=before_rev,
|
|
285
|
+
run_id=run_id,
|
|
286
|
+
reason=reason,
|
|
287
|
+
touched_entities=touched,
|
|
288
|
+
before_hash=before_hash,
|
|
289
|
+
after_hash=after_hash,
|
|
290
|
+
created_at=manifest["created_at"],
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _apply(snapshot: dict[str, list[dict]],
|
|
295
|
+
mutation: GraphMutation) -> dict[str, list[dict]]:
|
|
296
|
+
after: dict[str, list[dict]] = {
|
|
297
|
+
table: [dict(row) for row in rows] for table, rows in snapshot.items()
|
|
298
|
+
}
|
|
299
|
+
for table, upserts in mutation.upserts.items():
|
|
300
|
+
if table not in GRAPH_TABLES:
|
|
301
|
+
raise ValueError(f"unknown graph table {table!r}")
|
|
302
|
+
id_key = _TABLE_ID_KEY[table]
|
|
303
|
+
by_id = {row[id_key]: row for row in after[table]}
|
|
304
|
+
for row in upserts:
|
|
305
|
+
if id_key not in row:
|
|
306
|
+
raise ValueError(f"{table} upsert missing {id_key}: {row!r}")
|
|
307
|
+
by_id[row[id_key]] = dict(row)
|
|
308
|
+
after[table] = list(by_id.values())
|
|
309
|
+
for table, retire_ids in mutation.retire_ids.items():
|
|
310
|
+
if table not in GRAPH_TABLES:
|
|
311
|
+
raise ValueError(f"unknown graph table {table!r}")
|
|
312
|
+
id_key = _TABLE_ID_KEY[table]
|
|
313
|
+
retired = set(retire_ids)
|
|
314
|
+
after[table] = [row for row in after[table] if row[id_key] not in retired]
|
|
315
|
+
return after
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _atomic_write_text(path: Path, text: str) -> None:
|
|
319
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
321
|
+
tmp.write_text(text, encoding="utf-8")
|
|
322
|
+
os.replace(tmp, path)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _write_jsonl(path: Path, rows: list[dict]) -> None:
|
|
326
|
+
lines = "".join(
|
|
327
|
+
json.dumps(row, sort_keys=True, separators=(",", ":")) + "\n" for row in rows
|
|
328
|
+
)
|
|
329
|
+
path.write_text(lines, encoding="utf-8")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _write_json(path: Path, record: dict) -> None:
|
|
333
|
+
path.write_text(
|
|
334
|
+
json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
335
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Cross-entity integrity checks for the V2 evidence graph.
|
|
2
|
+
|
|
3
|
+
Reference integrity is a commit precondition: a Finding may not point at a
|
|
4
|
+
missing Study, an EvidenceLink at a missing Finding/Claim, an Audit at a
|
|
5
|
+
missing Study. Returns stable error strings; no printing from the engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# graph table name -> entity id field
|
|
12
|
+
_TABLE_ID_KEY = {
|
|
13
|
+
"sources": "source_id",
|
|
14
|
+
"studies": "study_id",
|
|
15
|
+
"findings": "finding_id",
|
|
16
|
+
"outcomes": "outcome_id",
|
|
17
|
+
"claims": "claim_id",
|
|
18
|
+
"evidence_links": "evidence_link_id",
|
|
19
|
+
"audits": "audit_id",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ids(rows: list[dict], table: str) -> set[str]:
|
|
24
|
+
key = _TABLE_ID_KEY[table]
|
|
25
|
+
return {row[key] for row in rows}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_graph(tables: dict[str, list[dict]]) -> list[str]:
|
|
29
|
+
"""Validate cross-entity references across all graph tables.
|
|
30
|
+
|
|
31
|
+
`tables` maps table name -> list of entity dicts (the in-memory graph
|
|
32
|
+
state being validated, already schema-validated per entity).
|
|
33
|
+
"""
|
|
34
|
+
errors: list[str] = []
|
|
35
|
+
|
|
36
|
+
sources = _ids(tables.get("sources", []), "sources")
|
|
37
|
+
studies = _ids(tables.get("studies", []), "studies")
|
|
38
|
+
findings = _ids(tables.get("findings", []), "findings")
|
|
39
|
+
outcomes = _ids(tables.get("outcomes", []), "outcomes")
|
|
40
|
+
claims = _ids(tables.get("claims", []), "claims")
|
|
41
|
+
links = tables.get("evidence_links", [])
|
|
42
|
+
audits = tables.get("audits", [])
|
|
43
|
+
|
|
44
|
+
for f in tables.get("findings", []):
|
|
45
|
+
if f["study_id"] not in studies:
|
|
46
|
+
errors.append(
|
|
47
|
+
f"finding {f['finding_id']} references missing study {f['study_id']}"
|
|
48
|
+
)
|
|
49
|
+
if f["outcome_id"] not in outcomes:
|
|
50
|
+
errors.append(
|
|
51
|
+
f"finding {f['finding_id']} references missing outcome {f['outcome_id']}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
for s in tables.get("studies", []):
|
|
55
|
+
for sid in s.get("source_ids", []):
|
|
56
|
+
if sid not in sources:
|
|
57
|
+
errors.append(
|
|
58
|
+
f"study {s['study_id']} references missing source {sid}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for link in links:
|
|
62
|
+
if link["finding_id"] not in findings:
|
|
63
|
+
errors.append(
|
|
64
|
+
f"evidence_link {link['evidence_link_id']} references missing "
|
|
65
|
+
f"finding {link['finding_id']}"
|
|
66
|
+
)
|
|
67
|
+
if link["claim_id"] not in claims:
|
|
68
|
+
errors.append(
|
|
69
|
+
f"evidence_link {link['evidence_link_id']} references missing "
|
|
70
|
+
f"claim {link['claim_id']}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for c in tables.get("claims", []):
|
|
74
|
+
for oid in c.get("primary_outcome_ids", []):
|
|
75
|
+
if oid not in outcomes:
|
|
76
|
+
errors.append(
|
|
77
|
+
f"claim {c['claim_id']} references missing outcome {oid}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
for a in audits:
|
|
81
|
+
if a["study_id"] not in studies:
|
|
82
|
+
errors.append(
|
|
83
|
+
f"methodology_audit {a['audit_id']} references missing "
|
|
84
|
+
f"study {a['study_id']}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return errors
|
package/engine/ids.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Stable entity ID helpers.
|
|
2
|
+
|
|
3
|
+
Project and Run IDs are **creation identities**, not question hashes: two
|
|
4
|
+
teams may intentionally create two independent Projects with the same
|
|
5
|
+
question, so uniqueness comes from UTC creation time plus a short entropy
|
|
6
|
+
suffix. The normalized question fingerprint is metadata only, never identity.
|
|
7
|
+
|
|
8
|
+
Local entity prefixes are frozen in the V2 design:
|
|
9
|
+
|
|
10
|
+
PRJ Project RUN Run SRC Source STU Study FND Finding OUT Outcome
|
|
11
|
+
CLM Claim LNK EvidenceLink AUD MethodologyAudit GAP KnowledgeGap
|
|
12
|
+
DSN StudyDesign DAT DatasetAsset APL AnalysisPlan ANL AnalysisRun
|
|
13
|
+
DEC DecisionSnapshot PIL PilotRun SYN LibrarySynthesis
|
|
14
|
+
|
|
15
|
+
Interpretive entities (Claim, EvidenceLink, KnowledgeGap, DecisionSnapshot)
|
|
16
|
+
keep stable Project-local IDs across edits; version/revision metadata records
|
|
17
|
+
change instead of silently re-identifying.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
import hashlib
|
|
22
|
+
import secrets
|
|
23
|
+
|
|
24
|
+
_LOCAL_PREFIXES = frozenset({
|
|
25
|
+
"SRC", "STU", "FND", "OUT", "CLM", "LNK", "AUD", "GAP",
|
|
26
|
+
"DSN", "DAT", "APL", "ANL", "DEC", "PIL", "SYN",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _utc_compact(now: datetime) -> str:
|
|
31
|
+
if now.tzinfo is None:
|
|
32
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
33
|
+
return now.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _question_fingerprint(question: str) -> str:
|
|
37
|
+
norm = " ".join(question.strip().lower().split())
|
|
38
|
+
return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:8]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _unique_suffix(existing: set[str]) -> str:
|
|
42
|
+
while True:
|
|
43
|
+
suffix = secrets.token_hex(4)
|
|
44
|
+
if suffix not in existing:
|
|
45
|
+
return suffix
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def new_project_id(question: str, *, now: datetime | None = None,
|
|
49
|
+
existing: set[str] | None = None) -> str:
|
|
50
|
+
"""Create a unique Project creation identity.
|
|
51
|
+
|
|
52
|
+
`existing` (when supplied) is the set of suffixes already in use, so a
|
|
53
|
+
caller can guarantee collision-free allocation within its namespace.
|
|
54
|
+
"""
|
|
55
|
+
now = now or datetime.now(timezone.utc)
|
|
56
|
+
existing = existing or set()
|
|
57
|
+
suffix = _unique_suffix(existing)
|
|
58
|
+
return f"PRJ-{_utc_compact(now)}-{_question_fingerprint(question)}-{suffix}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def new_run_id(now: datetime | None = None, existing: set[str] | None = None) -> str:
|
|
62
|
+
"""Create a unique Run creation identity."""
|
|
63
|
+
now = now or datetime.now(timezone.utc)
|
|
64
|
+
existing = existing or set()
|
|
65
|
+
suffix = _unique_suffix(existing)
|
|
66
|
+
return f"RUN-{_utc_compact(now)}-{suffix}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def new_local_id(prefix: str, existing: set[str]) -> str:
|
|
70
|
+
"""Create a unique local entity ID with a frozen design prefix."""
|
|
71
|
+
if prefix not in _LOCAL_PREFIXES:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"unknown entity prefix {prefix!r}; frozen prefixes: "
|
|
74
|
+
f"{sorted(_LOCAL_PREFIXES)}"
|
|
75
|
+
)
|
|
76
|
+
suffix = _unique_suffix(existing)
|
|
77
|
+
return f"{prefix}-{suffix}"
|