eduevidence 6.2.0 → 6.3.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/CHANGELOG.md +395 -0
- package/README.md +22 -13
- package/README.zh-CN.md +15 -8
- package/SKILL.md +10 -9
- package/benchmarks/evidence-library.json +277 -1
- package/docs/architecture.md +6 -3
- package/docs/j-ev-experimental.md +250 -0
- package/docs/reproducibility.md +138 -0
- package/domains/_neutral/copy/few_shots.json +21 -0
- package/domains/_neutral/copy/framing_lexicon.json +19 -0
- package/domains/_neutral/copy/module_labels.json +5 -0
- package/domains/_neutral/copy/module_labels_footer.json +102 -0
- package/domains/_neutral/copy/module_labels_modules.json +204 -0
- package/domains/_neutral/copy/module_labels_nav.json +126 -0
- package/domains/_neutral/copy/module_labels_summary.json +98 -0
- package/domains/_neutral/copy/module_labels_tables.json +164 -0
- package/domains/_neutral/copy/module_labels_v2.json +90 -0
- package/domains/_neutral/copy/risk_constructs.json +20 -0
- package/domains/_neutral/copy/section_titles.json +66 -0
- package/domains/_neutral/copy/terminology.json +11 -0
- package/domains/check_copy_packs.py +103 -0
- package/domains/education/copy/few_shots.json +22 -0
- package/domains/education/copy/framing_enums.json +167 -0
- package/domains/education/copy/framing_lexicon.json +166 -0
- package/domains/education/copy/module_labels.json +169 -0
- package/domains/education/copy/risk_constructs.json +48 -0
- package/domains/education/copy/section_titles.json +186 -0
- package/domains/education/copy/terminology.json +70 -0
- package/domains/education/manifest.json +1 -1
- package/domains/education/outcome_taxonomy.json +2 -2
- package/domains/manifest.json +1 -1
- package/domains/policy/copy/few_shots.json +22 -0
- package/domains/policy/copy/framing_enums.json +94 -0
- package/domains/policy/copy/framing_lexicon.json +174 -0
- package/domains/policy/copy/module_labels.json +168 -0
- package/domains/policy/copy/risk_constructs.json +33 -0
- package/domains/policy/copy/section_titles.json +186 -0
- package/domains/policy/copy/terminology.json +64 -0
- package/engine/capabilities.py +57 -5
- package/engine/decision_policy.py +88 -17
- package/engine/library_builtin.py +7 -4
- package/engine/tribunal.py +17 -23
- package/engine/versions.py +1 -1
- package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +4 -4
- package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
- package/examples/spaced-retrieval-practice/report.html +2522 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +4 -4
- package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +36 -36
- package/integrations/jev/__init__.py +115 -0
- package/integrations/jev/approval.py +212 -0
- package/integrations/jev/cli.py +84 -0
- package/integrations/jev/config.py +112 -0
- package/integrations/jev/gateway.py +128 -0
- package/integrations/jev/modes.py +38 -0
- package/integrations/jev/tools_classify.py +88 -0
- package/integrations/jev/tools_extract.py +111 -0
- package/integrations/jev/tools_rerank.py +71 -0
- package/integrations/jev/tools_screen.py +87 -0
- package/integrations/jev/tools_verify.py +95 -0
- package/integrations/jev_mcp.py +22 -0
- package/integrations/semantic_decide.py +286 -0
- package/integrations/semdecide_cli.py +55 -0
- package/package.json +9 -1
- package/pyproject.toml +1 -1
- package/references/report-copy-style.md +43 -3
- package/schemas/v2/decision-snapshot.schema.json +20 -9
- package/schemas/v2/intake.schema.json +191 -0
- package/scripts/build_evidence_library.py +15 -5
- package/scripts/dashboard_server.py +13 -2
- package/scripts/intake/__init__.py +31 -0
- package/scripts/intake/__main__.py +18 -0
- package/scripts/intake/background.py +78 -0
- package/scripts/intake/browser.py +79 -0
- package/scripts/intake/cli.py +57 -0
- package/scripts/intake/constants.py +57 -0
- package/scripts/intake/depth.py +53 -0
- package/scripts/intake/enhancements.py +106 -0
- package/scripts/intake/hooks.py +90 -0
- package/scripts/intake/prefs.py +76 -0
- package/scripts/intake/prompts.py +85 -0
- package/scripts/intake/session.py +152 -0
- package/scripts/lint_file_layers.py +126 -0
- package/scripts/orchestrator.py +68 -17
- package/scripts/pre_verdict_gate.py +21 -7
- package/scripts/skill_lint.py +11 -1
- package/scripts/skill_payload.py +3 -3
- package/scripts/test_adversarial_empirical.py +70 -6
- package/skill/agents/evidence-judge.md +49 -7
- package/skill/workflows/experimental-jev.md +170 -0
- package/skill/workflows/intake.md +120 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +32 -14
- package/visualization/eduevidence-report/scripts/build_report.py +75 -662
- package/visualization/eduevidence-report/scripts/report_copy_pack.py +296 -0
- package/visualization/eduevidence-report/scripts/report_copy_policy_guard.py +47 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +61 -0
- package/scripts/build_esl_artifacts.py +0 -1921
- package/scripts/build_killer_demo.py +0 -295
- package/scripts/enrich_projects_human_and_lieflat.py +0 -315
- package/scripts/generate_new_projects.py +0 -686
- package/scripts/sync_killer_demo_report.py +0 -270
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Round-2 enhancement blocks: Agent MCP / Jev / SemDecide."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from .constants import ENHANCEMENTS
|
|
7
|
+
from .prompts import ask, ask_multi, ask_yes, split_csv
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_enhancements(enhancements: list[str] | None) -> list[str]:
|
|
11
|
+
if not enhancements:
|
|
12
|
+
return ["none"]
|
|
13
|
+
alias = {"mcp": "agent_mcp", "agent-mcp": "agent_mcp",
|
|
14
|
+
"无": "none", "不启用": "none"}
|
|
15
|
+
out: list[str] = []
|
|
16
|
+
for e in enhancements:
|
|
17
|
+
key = alias.get(str(e).strip().lower(), str(e).strip().lower())
|
|
18
|
+
if key in ENHANCEMENTS and key not in out:
|
|
19
|
+
out.append(key)
|
|
20
|
+
if not out:
|
|
21
|
+
return ["none"]
|
|
22
|
+
if "none" in out and len(out) > 1:
|
|
23
|
+
return ["none"]
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def collect_round1_enhancements(*, prefs: dict[str, Any],
|
|
28
|
+
enhancements: list[str] | None,
|
|
29
|
+
interactive: bool,
|
|
30
|
+
printer: Callable[[str], None] = print) -> list[str]:
|
|
31
|
+
from .constants import ENHANCEMENT_BLURBS
|
|
32
|
+
|
|
33
|
+
if interactive and enhancements is None:
|
|
34
|
+
printer("—— 第一轮 ——")
|
|
35
|
+
printer("2) 执行增强(可多选):")
|
|
36
|
+
for key, blurb in ENHANCEMENT_BLURBS.items():
|
|
37
|
+
printer(f" [{key}] {blurb}")
|
|
38
|
+
default_enh = [e for e in (prefs.get("enhancement") or ["none"])
|
|
39
|
+
if e in ENHANCEMENTS] or ["none"]
|
|
40
|
+
enhancements = ask_multi("请选择增强", ENHANCEMENTS, default_enh)
|
|
41
|
+
elif enhancements is None:
|
|
42
|
+
enhancements = list(prefs.get("enhancement") or ["none"])
|
|
43
|
+
return normalize_enhancements(enhancements)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _collect_mcp(prefs: dict[str, Any], *, interactive: bool,
|
|
47
|
+
mcp_table: list[dict[str, str]] | None,
|
|
48
|
+
printer: Callable[[str], None]) -> dict[str, Any]:
|
|
49
|
+
role_mapping: dict[str, Any] = {}
|
|
50
|
+
if mcp_table:
|
|
51
|
+
printer(" MCP 角色 → CLI / 模型 授权表:")
|
|
52
|
+
for row in mcp_table:
|
|
53
|
+
printer(f" {row.get('role', '?')} → {row.get('cli', '?')} / {row.get('model', '?')}")
|
|
54
|
+
if row.get("role") and row.get("cli") and row.get("model"):
|
|
55
|
+
role_mapping[str(row["role"])] = {
|
|
56
|
+
"cli": str(row["cli"]), "model": str(row["model"])}
|
|
57
|
+
elif (prefs.get("mcp") or {}).get("role_mapping"):
|
|
58
|
+
role_mapping = dict((prefs.get("mcp") or {}).get("role_mapping") or {})
|
|
59
|
+
printer(" 复用 prefs 中的 MCP 角色映射(可在批准文件中复核)。")
|
|
60
|
+
authorized = True
|
|
61
|
+
if interactive and mcp_table:
|
|
62
|
+
authorized = ask_yes(" 采用上述角色→CLI/模型表并授权?(Y/n)", True)
|
|
63
|
+
elif interactive:
|
|
64
|
+
authorized = ask_yes(" 启用 Agent MCP 增强(角色表将由批准流程生成)?(Y/n)", True)
|
|
65
|
+
return {"authorized": authorized, "role_mapping": role_mapping,
|
|
66
|
+
"note": "role table persisted via agent_mcp approval when authorized"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _collect_jev(prefs: dict[str, Any], *, interactive: bool,
|
|
70
|
+
printer: Callable[[str], None]) -> dict[str, Any]:
|
|
71
|
+
default_tools = list((prefs.get("jev") or {}).get("tool_subset") or
|
|
72
|
+
["search", "fetch", "validate"])
|
|
73
|
+
tools = default_tools
|
|
74
|
+
if interactive:
|
|
75
|
+
printer(" Jev 工具子集(只派发确定性小步;缺省 search/fetch/validate)")
|
|
76
|
+
tools = split_csv(ask(" 工具子集(逗号分隔)", ",".join(default_tools)))
|
|
77
|
+
return {"tool_subset": tools or default_tools,
|
|
78
|
+
"note": "Jev dispatch stays limited to this tool subset"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _collect_sem(interactive: bool, printer: Callable[[str], None]) -> dict[str, Any]:
|
|
82
|
+
default_purpose = "结构化上游 ResearchIntent / Frame,供 recommend_mode 使用"
|
|
83
|
+
purpose = default_purpose
|
|
84
|
+
if interactive:
|
|
85
|
+
printer(" SemDecide 用途(只结构化上游意图,不改下游确定性路由)")
|
|
86
|
+
purpose = ask(" 用途说明", default_purpose) or default_purpose
|
|
87
|
+
return {"purpose": purpose,
|
|
88
|
+
"note": "SemDecide feeds intent only; mode_router stays deterministic"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def collect_round2_enhancements(
|
|
92
|
+
enhancements: list[str], *, prefs: dict[str, Any], interactive: bool,
|
|
93
|
+
mcp_table: list[dict[str, str]] | None = None,
|
|
94
|
+
printer: Callable[[str], None] = print,
|
|
95
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, dict[str, Any] | None]:
|
|
96
|
+
"""Return (mcp_block, jev_block, sem_block); None when that enhancement is off."""
|
|
97
|
+
wants_mcp = "agent_mcp" in enhancements
|
|
98
|
+
wants_jev = "jev" in enhancements
|
|
99
|
+
wants_sem = "semdecide" in enhancements
|
|
100
|
+
if interactive and (wants_mcp or wants_jev or wants_sem):
|
|
101
|
+
printer("—— 第二轮(仅增强相关)——")
|
|
102
|
+
mcp = _collect_mcp(prefs, interactive=interactive, mcp_table=mcp_table,
|
|
103
|
+
printer=printer) if wants_mcp else None
|
|
104
|
+
jev = _collect_jev(prefs, interactive=interactive, printer=printer) if wants_jev else None
|
|
105
|
+
sem = _collect_sem(interactive, printer) if wants_sem else None
|
|
106
|
+
return mcp, jev, sem
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Thin orchestrator hooks: prepare run context, finish with optional open."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .browser import maybe_open_after_run, resolve_main_report
|
|
10
|
+
from .prompts import is_interactive
|
|
11
|
+
from .session import run_intake
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def prepare_run(
|
|
15
|
+
*,
|
|
16
|
+
question: str,
|
|
17
|
+
depth: str | None = None,
|
|
18
|
+
enhancements: list[str] | None = None,
|
|
19
|
+
domain: str = "education",
|
|
20
|
+
assume_yes: bool = False,
|
|
21
|
+
fallback_depth: str = "M",
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Collect intake (or fall back silently). Return orchestrator inputs.
|
|
24
|
+
|
|
25
|
+
Keys: question, depth, enhancements, intake_record, wants_mcp, error.
|
|
26
|
+
"""
|
|
27
|
+
interactive = (not assume_yes) and is_interactive()
|
|
28
|
+
try:
|
|
29
|
+
record = run_intake(
|
|
30
|
+
question=question,
|
|
31
|
+
depth=depth,
|
|
32
|
+
enhancements=enhancements,
|
|
33
|
+
domain=domain,
|
|
34
|
+
interactive=interactive,
|
|
35
|
+
assume_yes=assume_yes,
|
|
36
|
+
)
|
|
37
|
+
except ValueError as exc:
|
|
38
|
+
return {"error": str(exc)}
|
|
39
|
+
return {
|
|
40
|
+
"question": record["research_question"],
|
|
41
|
+
"depth": record["depth"],
|
|
42
|
+
"enhancements": list(record["enhancements"]),
|
|
43
|
+
"intake_record": record,
|
|
44
|
+
"wants_mcp": ("agent_mcp" in record["enhancements"]
|
|
45
|
+
and "none" not in record["enhancements"]),
|
|
46
|
+
"error": None,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def write_intake_artifact(run_dir: Path | str, intake_record: dict[str, Any] | None) -> None:
|
|
51
|
+
if not intake_record:
|
|
52
|
+
return
|
|
53
|
+
_validate_intake_record(intake_record)
|
|
54
|
+
path = Path(run_dir) / "intake.json"
|
|
55
|
+
path.write_text(json.dumps(intake_record, ensure_ascii=False, indent=2) + "\n",
|
|
56
|
+
encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _validate_intake_record(record: dict[str, Any]) -> None:
|
|
60
|
+
try:
|
|
61
|
+
import jsonschema
|
|
62
|
+
except ImportError:
|
|
63
|
+
return
|
|
64
|
+
schema_path = (Path(__file__).resolve().parents[2]
|
|
65
|
+
/ "schemas" / "v2" / "intake.schema.json")
|
|
66
|
+
if not schema_path.is_file():
|
|
67
|
+
return
|
|
68
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
69
|
+
try:
|
|
70
|
+
jsonschema.validate(record, schema)
|
|
71
|
+
except jsonschema.ValidationError as exc:
|
|
72
|
+
raise ValueError(f"intake record failed schema validation: {exc.message}") from exc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def after_run(
|
|
76
|
+
run_dir: Path | str,
|
|
77
|
+
*,
|
|
78
|
+
intake_record: dict[str, Any] | None,
|
|
79
|
+
summary: dict[str, Any],
|
|
80
|
+
printer: Callable[[str], None] = print,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Open the main report when the run completed and prefs.open_browser is set."""
|
|
83
|
+
if summary.get("failures") or not summary.get("completed_all"):
|
|
84
|
+
return
|
|
85
|
+
theme = (intake_record or {}).get("default_main_theme")
|
|
86
|
+
if not maybe_open_after_run(run_dir, theme=theme):
|
|
87
|
+
return
|
|
88
|
+
report = resolve_main_report(run_dir, theme)
|
|
89
|
+
if report is not None:
|
|
90
|
+
printer(f"opened report: {report}")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Durable non-question prefs at ~/.eduevidence/prefs.json."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .constants import DEPTH_CHOICES, PREFS_NAME, SCHEMA_VERSION, THEME_NAMES
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def prefs_path() -> Path:
|
|
13
|
+
"""~/.eduevidence/prefs.json (EDUEVIDENCE_HOME wins)."""
|
|
14
|
+
home = Path(os.environ.get("EDUEVIDENCE_HOME", "~/.eduevidence")).expanduser()
|
|
15
|
+
return home / PREFS_NAME
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def default_prefs() -> dict[str, Any]:
|
|
19
|
+
return {
|
|
20
|
+
"schema_version": SCHEMA_VERSION,
|
|
21
|
+
"enhancement": ["none"],
|
|
22
|
+
"mcp": {"role_mapping": {}},
|
|
23
|
+
"jev": {"tool_subset": []},
|
|
24
|
+
"semdecide": {"purpose": ""},
|
|
25
|
+
"depth_preference": "auto",
|
|
26
|
+
"open_browser": True,
|
|
27
|
+
"default_main_theme": "claude",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _sanitize(prefs: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
prefs.pop("question", None)
|
|
33
|
+
prefs.pop("research_question", None)
|
|
34
|
+
if prefs.get("default_main_theme") not in THEME_NAMES:
|
|
35
|
+
prefs["default_main_theme"] = "claude"
|
|
36
|
+
if prefs.get("depth_preference") not in DEPTH_CHOICES:
|
|
37
|
+
prefs["depth_preference"] = "auto"
|
|
38
|
+
enh = prefs.get("enhancement") or ["none"]
|
|
39
|
+
if "none" in enh and len(enh) > 1:
|
|
40
|
+
enh = ["none"]
|
|
41
|
+
prefs["enhancement"] = enh or ["none"]
|
|
42
|
+
return prefs
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_prefs() -> dict[str, Any]:
|
|
46
|
+
"""Load prefs; missing/corrupt → defaults. Never contains a question."""
|
|
47
|
+
path = prefs_path()
|
|
48
|
+
prefs = default_prefs()
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
except (OSError, json.JSONDecodeError):
|
|
52
|
+
return prefs
|
|
53
|
+
if not isinstance(data, dict):
|
|
54
|
+
return prefs
|
|
55
|
+
for key in ("schema_version", "enhancement", "mcp", "jev", "semdecide",
|
|
56
|
+
"depth_preference", "open_browser", "default_main_theme"):
|
|
57
|
+
if key in data:
|
|
58
|
+
prefs[key] = data[key]
|
|
59
|
+
return _sanitize(prefs)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_prefs(prefs: dict[str, Any]) -> Path:
|
|
63
|
+
"""Persist non-question prefs. Refuses to write a question field.
|
|
64
|
+
|
|
65
|
+
Atomic write: temp file + os.replace so a crash cannot leave a torn prefs.json.
|
|
66
|
+
"""
|
|
67
|
+
payload = {k: v for k, v in prefs.items()
|
|
68
|
+
if k not in ("question", "research_question")}
|
|
69
|
+
payload.setdefault("schema_version", SCHEMA_VERSION)
|
|
70
|
+
path = prefs_path()
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
tmp = path.with_suffix(".json.tmp")
|
|
73
|
+
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
74
|
+
encoding="utf-8")
|
|
75
|
+
os.replace(tmp, path)
|
|
76
|
+
return path
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""TTY prompt helpers and the fixed two-round prompt script."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_interactive() -> bool:
|
|
9
|
+
try:
|
|
10
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
11
|
+
except Exception:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ask(prompt: str, default: str | None = None) -> str:
|
|
16
|
+
"""Single-line ask; empty input keeps default. Never invents a default."""
|
|
17
|
+
suffix = f" [{default}]" if default not in (None, "") else ""
|
|
18
|
+
try:
|
|
19
|
+
raw = input(f"{prompt}{suffix}: ").strip()
|
|
20
|
+
except EOFError:
|
|
21
|
+
return default or ""
|
|
22
|
+
return raw or (default or "")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ask_yes(prompt: str, default_yes: bool = True) -> bool:
|
|
26
|
+
default = "Y" if default_yes else "n"
|
|
27
|
+
ans = ask(prompt, default).lower()
|
|
28
|
+
return ans not in ("n", "no")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ask_multi(prompt: str, allowed: tuple[str, ...], default: list[str]) -> list[str]:
|
|
32
|
+
"""Comma-separated multi-select against `allowed`."""
|
|
33
|
+
legend = "/".join(allowed)
|
|
34
|
+
raw = ask(f"{prompt}(可多选,逗号分隔,{legend})",
|
|
35
|
+
",".join(default) if default else None)
|
|
36
|
+
if not raw:
|
|
37
|
+
return list(default) or ["none"]
|
|
38
|
+
alias = {"mcp": "agent_mcp", "agent-mcp": "agent_mcp",
|
|
39
|
+
"否": "none", "无": "none", "不启用": "none"}
|
|
40
|
+
picked: list[str] = []
|
|
41
|
+
for token in re.split(r"[,,\s]+", raw):
|
|
42
|
+
t = token.strip().lower()
|
|
43
|
+
if not t:
|
|
44
|
+
continue
|
|
45
|
+
key = alias.get(t, t)
|
|
46
|
+
if key in allowed and key not in picked:
|
|
47
|
+
picked.append(key)
|
|
48
|
+
if not picked:
|
|
49
|
+
return list(default) or ["none"]
|
|
50
|
+
if "none" in picked and len(picked) > 1:
|
|
51
|
+
return ["none"]
|
|
52
|
+
return picked
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def split_csv(raw: str) -> list[str]:
|
|
56
|
+
return [t.strip() for t in re.split(r"[,,\s]+", raw or "") if t.strip()]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
PROMPT_SCRIPT = """\
|
|
60
|
+
EduEvidence 一次性两轮引导(固定语句,用户只答模式和问题)
|
|
61
|
+
|
|
62
|
+
—— 第一轮 ——
|
|
63
|
+
1) 研究问题(必填)
|
|
64
|
+
2) 执行增强(可多选,各附一句话简介):
|
|
65
|
+
[agent_mcp] Agent MCP — 多 CLI/多模型分工、独立子上下文、超时恢复(可选执行增强)
|
|
66
|
+
[jev] Jev — 轻量工具子集派发,适合把确定性小步交给受限工具面
|
|
67
|
+
[semdecide] SemDecide — 语义决策辅助,只结构化上游意图,不改下游确定性路由
|
|
68
|
+
[none] 均不启用 — 平台原生模式,零外部执行增强
|
|
69
|
+
3) 深度:
|
|
70
|
+
[S] S 快检 — 单点问题,最小检索面,直接给边界结论
|
|
71
|
+
[M] M 标准 — 常规采用/比较问题,标准证据到决策流程
|
|
72
|
+
[L] L 深研 — 高影响、有争议或要试点,完整深研与决策扩展
|
|
73
|
+
[auto] 自动 — 单点→S,常规采用→M,高影响/争议/要试点→L,不确定取 M
|
|
74
|
+
|
|
75
|
+
—— 第二轮(仅增强开启时的增强项;背景深挖始终执行)——
|
|
76
|
+
· Agent MCP 开 → 角色 → CLI / 模型 授权表(只展示已扫描到的真实 CLI/模型)
|
|
77
|
+
· Jev 开 → 工具子集(search / fetch / validate / …)
|
|
78
|
+
· SemDecide 开 → 用途说明(只结构化上游意图)
|
|
79
|
+
· 背景智能深挖:按题目推断领域 Frame 骨架后,针对该题追问 3–6 个影响边界的问题
|
|
80
|
+
(人群 / 干预 / 对照 / 主结果 / 情境),智能默认 + 一次确认,禁止编造。
|
|
81
|
+
|
|
82
|
+
—— 收尾 ——
|
|
83
|
+
摘要一次确认后无人值守。prefs 只记 enhancement / mcp·jev 映射 / depth /
|
|
84
|
+
open_browser / default_main_theme,不记研究问题。五主题全渲染,主报告只决定打开哪份。
|
|
85
|
+
"""
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""One-shot two-round intake session (question → mode → confirm → unattended)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from .background import collect_frame_hints
|
|
7
|
+
from .constants import DEPTH_BLURBS, DEPTH_CHOICES, FRAME_SLOTS, SCHEMA_VERSION, THEME_NAMES
|
|
8
|
+
from .depth import normalize_depth_choice, resolve_depth
|
|
9
|
+
from .enhancements import collect_round1_enhancements, collect_round2_enhancements
|
|
10
|
+
from .prefs import load_prefs, prefs_path, save_prefs
|
|
11
|
+
from .prompts import ask, ask_yes, is_interactive
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _round1_question(question: str | None, *, interactive: bool) -> str:
|
|
15
|
+
q = (question or "").strip()
|
|
16
|
+
if interactive and not q:
|
|
17
|
+
q = ask("1) 研究问题(必填)")
|
|
18
|
+
if not q:
|
|
19
|
+
raise ValueError("research question is required (round 1)")
|
|
20
|
+
return q
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _round1_depth(depth: str | None, *, prefs: dict[str, Any],
|
|
24
|
+
interactive: bool,
|
|
25
|
+
printer: Callable[[str], None]) -> str | None:
|
|
26
|
+
depth_choice = normalize_depth_choice(depth)
|
|
27
|
+
if interactive and depth_choice is None:
|
|
28
|
+
printer("3) 深度:")
|
|
29
|
+
for key, blurb in DEPTH_BLURBS.items():
|
|
30
|
+
printer(f" [{key}] {blurb}")
|
|
31
|
+
picked = ask("请选择深度 (S/M/L/auto)", prefs.get("depth_preference") or "auto")
|
|
32
|
+
depth_choice = picked if picked in DEPTH_CHOICES else "auto"
|
|
33
|
+
return depth_choice
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _wrap_up(*, question: str, enhancements: list[str], depth_resolved: str,
|
|
37
|
+
depth_why: str, frame_hints: dict[str, Any],
|
|
38
|
+
open_browser_flag: bool, theme: str, interactive: bool,
|
|
39
|
+
printer: Callable[[str], None]) -> bool:
|
|
40
|
+
if not interactive:
|
|
41
|
+
return True
|
|
42
|
+
printer("—— 收尾摘要(一次确认后无人值守)——")
|
|
43
|
+
printer(f" 问题:{question}")
|
|
44
|
+
printer(f" 增强:{', '.join(enhancements)}")
|
|
45
|
+
printer(f" 深度:{depth_resolved}({depth_why})")
|
|
46
|
+
printer(f" 边界:{ {k: frame_hints.get(k) for k in FRAME_SLOTS} }")
|
|
47
|
+
printer(f" 结束后打开报告:{'是' if open_browser_flag else '否'} | 主主题:{theme}")
|
|
48
|
+
confirmed = ask_yes(" 确认并开始无人值守执行?(Y/n)", True)
|
|
49
|
+
if not confirmed:
|
|
50
|
+
raise ValueError("intake summary not confirmed; abort before unattended run")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _persist_prefs(enhancements: list[str], mcp_block: dict[str, Any] | None,
|
|
55
|
+
jev_block: dict[str, Any] | None, sem_block: dict[str, Any] | None,
|
|
56
|
+
prefs: dict[str, Any], *, depth_choice: str,
|
|
57
|
+
open_browser_flag: bool, theme: str) -> None:
|
|
58
|
+
next_prefs = {
|
|
59
|
+
"schema_version": SCHEMA_VERSION,
|
|
60
|
+
"enhancement": enhancements,
|
|
61
|
+
"mcp": {"role_mapping": (mcp_block or {}).get("role_mapping")
|
|
62
|
+
or (prefs.get("mcp") or {}).get("role_mapping") or {}},
|
|
63
|
+
"jev": {"tool_subset": (jev_block or {}).get("tool_subset")
|
|
64
|
+
or (prefs.get("jev") or {}).get("tool_subset") or []},
|
|
65
|
+
"depth_preference": depth_choice,
|
|
66
|
+
"open_browser": open_browser_flag,
|
|
67
|
+
"default_main_theme": theme,
|
|
68
|
+
}
|
|
69
|
+
if sem_block and sem_block.get("purpose"):
|
|
70
|
+
next_prefs["semdecide"] = {"purpose": sem_block["purpose"]}
|
|
71
|
+
save_prefs(next_prefs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run_intake(
|
|
75
|
+
question: str | None = None,
|
|
76
|
+
*,
|
|
77
|
+
depth: str | None = None,
|
|
78
|
+
enhancements: list[str] | None = None,
|
|
79
|
+
domain: str = "education",
|
|
80
|
+
interactive: bool | None = None,
|
|
81
|
+
assume_yes: bool = False,
|
|
82
|
+
open_browser: bool | None = None,
|
|
83
|
+
default_main_theme: str | None = None,
|
|
84
|
+
mcp_table: list[dict[str, str]] | None = None,
|
|
85
|
+
persist_prefs: bool = True,
|
|
86
|
+
printer: Callable[[str], None] = print,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Run the one-shot two-round intake and return a validated intake record.
|
|
89
|
+
|
|
90
|
+
- `assume_yes` / non-TTY → silent: prefs + explicit args only, no prompts.
|
|
91
|
+
- `interactive` forces TTY behaviour (tests can inject `printer` + stdin).
|
|
92
|
+
- User answers only mode and questions; everything else is inferred with
|
|
93
|
+
hard no-fabrication rules.
|
|
94
|
+
"""
|
|
95
|
+
prefs = load_prefs()
|
|
96
|
+
if interactive is None:
|
|
97
|
+
interactive = (not assume_yes) and is_interactive()
|
|
98
|
+
|
|
99
|
+
q = _round1_question(question, interactive=interactive)
|
|
100
|
+
enhancements = collect_round1_enhancements(
|
|
101
|
+
prefs=prefs, enhancements=enhancements,
|
|
102
|
+
interactive=interactive, printer=printer)
|
|
103
|
+
depth_choice = _round1_depth(depth, prefs=prefs, interactive=interactive,
|
|
104
|
+
printer=printer)
|
|
105
|
+
|
|
106
|
+
mcp_block, jev_block, sem_block = collect_round2_enhancements(
|
|
107
|
+
enhancements, prefs=prefs, interactive=interactive,
|
|
108
|
+
mcp_table=mcp_table, printer=printer)
|
|
109
|
+
|
|
110
|
+
frame_hints = collect_frame_hints(q, domain=domain, interactive=interactive,
|
|
111
|
+
printer=printer)
|
|
112
|
+
|
|
113
|
+
depth_choice, depth_resolved, depth_why = resolve_depth(
|
|
114
|
+
depth_choice if depth_choice in DEPTH_CHOICES else (depth_choice or None),
|
|
115
|
+
q, frame_hints, prefs)
|
|
116
|
+
|
|
117
|
+
open_browser_flag = (prefs.get("open_browser", True) if open_browser is None
|
|
118
|
+
else bool(open_browser))
|
|
119
|
+
theme = default_main_theme or prefs.get("default_main_theme") or "claude"
|
|
120
|
+
if theme not in THEME_NAMES:
|
|
121
|
+
theme = "claude"
|
|
122
|
+
|
|
123
|
+
source = "interactive" if interactive else ("yes_flag" if assume_yes else "silent")
|
|
124
|
+
summary_confirmed = _wrap_up(
|
|
125
|
+
question=q, enhancements=enhancements, depth_resolved=depth_resolved,
|
|
126
|
+
depth_why=depth_why, frame_hints=frame_hints,
|
|
127
|
+
open_browser_flag=open_browser_flag, theme=theme,
|
|
128
|
+
interactive=interactive, printer=printer)
|
|
129
|
+
|
|
130
|
+
record: dict[str, Any] = {
|
|
131
|
+
"schema_version": SCHEMA_VERSION,
|
|
132
|
+
"research_question": q,
|
|
133
|
+
"enhancements": enhancements,
|
|
134
|
+
"depth": depth_resolved,
|
|
135
|
+
"depth_choice": depth_choice if depth_choice in DEPTH_CHOICES else "auto",
|
|
136
|
+
"depth_rationale": depth_why,
|
|
137
|
+
"mcp": mcp_block,
|
|
138
|
+
"jev": jev_block,
|
|
139
|
+
"semdecide": sem_block,
|
|
140
|
+
"frame_hints": frame_hints,
|
|
141
|
+
"summary_confirmed": summary_confirmed,
|
|
142
|
+
"open_browser": open_browser_flag,
|
|
143
|
+
"default_main_theme": theme,
|
|
144
|
+
"interactive": bool(interactive),
|
|
145
|
+
"source": source,
|
|
146
|
+
}
|
|
147
|
+
if persist_prefs:
|
|
148
|
+
_persist_prefs(enhancements, mcp_block, jev_block, sem_block, prefs,
|
|
149
|
+
depth_choice=record["depth_choice"],
|
|
150
|
+
open_browser_flag=open_browser_flag, theme=theme)
|
|
151
|
+
record["prefs_path"] = str(prefs_path())
|
|
152
|
+
return record
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""scripts/lint_file_layers.py — file-layer hard gate.
|
|
3
|
+
|
|
4
|
+
Scans engine/ scripts/ integrations/ skill/ visualization/eduevidence-report/scripts/
|
|
5
|
+
for *.py *.js *.ts and enforces:
|
|
6
|
+
|
|
7
|
+
* single file > 300 lines → ERROR (monument / legacy → WARNING only)
|
|
8
|
+
* new / moved files must be ≤ 300 lines (not eligible for whitelists)
|
|
9
|
+
* new / moved files must be ≤ 300 lines (not eligible for the whitelist)
|
|
10
|
+
|
|
11
|
+
Monument whitelist = build_report.py, orchestrator.py, agent_mcp.py,
|
|
12
|
+
pre_verdict_gate.py, living.py, evidence_graph.py. Anything else over the
|
|
13
|
+
budget is an ERROR unless monument/legacy WARNING. Exit 1 on any ERROR.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 scripts/lint_file_layers.py
|
|
17
|
+
Exit code 1 when any ERROR.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
|
|
26
|
+
MAX_LINES = 300
|
|
27
|
+
|
|
28
|
+
SCAN_ROOTS = (
|
|
29
|
+
"engine",
|
|
30
|
+
"scripts",
|
|
31
|
+
"integrations",
|
|
32
|
+
"skill",
|
|
33
|
+
"visualization/eduevidence-report/scripts",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Historical monument whitelist — WARNING only (not ERROR).
|
|
37
|
+
# ONLY these six pre-split giants are grandfathered. New or moved files
|
|
38
|
+
# MUST NOT be added here — keep them ≤ MAX_LINES instead.
|
|
39
|
+
MONUMENT_BASENAMES = {
|
|
40
|
+
"build_report.py",
|
|
41
|
+
"orchestrator.py",
|
|
42
|
+
"agent_mcp.py",
|
|
43
|
+
"pre_verdict_gate.py",
|
|
44
|
+
"living.py",
|
|
45
|
+
"evidence_graph.py",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Pre-existing large files (before the layering gate). WARNING only.
|
|
49
|
+
# New/moved files must never be added here — keep them ≤ MAX_LINES.
|
|
50
|
+
LEGACY_LARGE_BASENAMES = {
|
|
51
|
+
"analysis.py", "core.py", "runner.py", "graph_store.py", "library_builtin.py",
|
|
52
|
+
"meta_analysis.py", "migration.py", "orchestration.py", "pilot.py",
|
|
53
|
+
"studio_read_model.py", "tribunal.py", "benchmark_evaluator.py",
|
|
54
|
+
"benchmark_judge.py", "benchmark_v2.py", "benchmark_v3.py",
|
|
55
|
+
"build_esl_artifacts.py", "build_evidence_library.py", "build_result.py",
|
|
56
|
+
"check_protocol_alignment.py", "dashboard_server.py",
|
|
57
|
+
"enrich_projects_human_and_lieflat.py", "generate_new_projects.py",
|
|
58
|
+
"render_report_html.py", "research_auto_cli.py", "run_workspace.py",
|
|
59
|
+
"test_adversarial_empirical.py", "build_figures.py", "charts_data.py",
|
|
60
|
+
"lieflat_engine.py", "zh_labels.py", "library.py",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
EXTS = {".py", ".js", ".ts"}
|
|
64
|
+
SKIP_DIRS = {"__pycache__", "node_modules", ".git", ".venv", "venv"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _iter_sources() -> list[Path]:
|
|
68
|
+
found: list[Path] = []
|
|
69
|
+
for root_name in SCAN_ROOTS:
|
|
70
|
+
root = ROOT / root_name
|
|
71
|
+
if not root.is_dir():
|
|
72
|
+
continue
|
|
73
|
+
for path in sorted(root.rglob("*")):
|
|
74
|
+
if not path.is_file() or path.suffix not in EXTS:
|
|
75
|
+
continue
|
|
76
|
+
if any(part in SKIP_DIRS for part in path.parts):
|
|
77
|
+
continue
|
|
78
|
+
found.append(path)
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_file_layers() -> tuple[list[str], list[str]]:
|
|
83
|
+
"""Return (errors, warnings)."""
|
|
84
|
+
errors: list[str] = []
|
|
85
|
+
warnings: list[str] = []
|
|
86
|
+
for path in _iter_sources():
|
|
87
|
+
rel = path.relative_to(ROOT).as_posix()
|
|
88
|
+
try:
|
|
89
|
+
n = sum(1 for _ in path.open(encoding="utf-8", errors="replace"))
|
|
90
|
+
except OSError as exc:
|
|
91
|
+
errors.append(f"{rel}: unreadable ({exc})")
|
|
92
|
+
continue
|
|
93
|
+
if n <= MAX_LINES:
|
|
94
|
+
continue
|
|
95
|
+
msg = f"{rel}: {n} lines > {MAX_LINES}"
|
|
96
|
+
if path.name in MONUMENT_BASENAMES:
|
|
97
|
+
warnings.append(
|
|
98
|
+
f"{msg} (monument whitelist — WARNING only; split when touched)")
|
|
99
|
+
elif path.name in LEGACY_LARGE_BASENAMES:
|
|
100
|
+
warnings.append(
|
|
101
|
+
f"{msg} (legacy large file — WARNING only; split when touched)")
|
|
102
|
+
else:
|
|
103
|
+
errors.append(
|
|
104
|
+
f"{msg} (not on monument whitelist; new/moved must be ≤ {MAX_LINES})")
|
|
105
|
+
return errors, warnings
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main() -> int:
|
|
109
|
+
print("[*] Running file-layer hard gate "
|
|
110
|
+
f"(max {MAX_LINES} lines; monuments+legacy WARNING only: "
|
|
111
|
+
f"{', '.join(sorted(MONUMENT_BASENAMES))})...")
|
|
112
|
+
errors, warnings = check_file_layers()
|
|
113
|
+
for w in warnings:
|
|
114
|
+
print(f" ! {w}")
|
|
115
|
+
if errors:
|
|
116
|
+
print(f"[-] File-layer lint FAILED with {len(errors)} error(s):",
|
|
117
|
+
file=sys.stderr)
|
|
118
|
+
for e in errors:
|
|
119
|
+
print(f" • {e}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
print(f"[+] File-layer lint PASSED: {len(warnings)} monument warning(s), 0 error(s).")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
raise SystemExit(main())
|