ef1bd6dfa9
The report writer had no interpretation methodology at all: a local agent calling the jyotish skill can read the reference library, the report model could read nothing. It could only restate the bundle. - frontend/src/lib/report-interpretation-packs/ holds one general pack and one pack per report theme, distilled from the in-repo reference guides. They constrain wording and reasoning discipline (term modernisation, how to talk about relative strength and SAV scores, the reasoning errors to avoid, the banned phrasings) and never assert a chart fact. - The general pack rides INSIDE the cached system message so the cached prefix stays byte-identical across sections; the chapter pack follows it and summary calls get the general pack only. - Skill jyotish-personal-report goes to 1.1.0 (1.0.0 deprecated): the contract now names interpretiveFacts and themeNarrativeSeeds as a bounded fact layer and states that the knowledge pack is not a fact source and cannot raise certainty. - Telemetry records interpretiveFactCount and knowledgePackCharacters as numbers only; the counter never throws so telemetry cannot break a run. Tests lock every theme resolving a pack, the 3,000 character budget, a forbidden-substring scan (paths, module names, vendor names, artefact names), the byte-stable cache prefix, and that no evidence id or date appears in the static content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016P5RoqzmUQEbeC2qjAkeGr
98 lines
3.7 KiB
Python
98 lines
3.7 KiB
Python
"""Immutable package and policy contract for jyotish-personal-report."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PACKAGE_ROOT = ROOT / "skills/jyotish-personal-report"
|
|
# was versions/1.0.0; bumped 2026-09-01 when the skill contract gained the
|
|
# interpretive fact layer and the static interpretation packs.
|
|
ACTIVE_VERSION = "1.1.0"
|
|
VERSION_ROOT = PACKAGE_ROOT / f"versions/{ACTIVE_VERSION}"
|
|
REGISTRY = ROOT / "skills/skill-package-registry.json"
|
|
HASH_DOMAIN = b"jyotisha-skill-package-v1\0"
|
|
|
|
|
|
def _update_length_prefixed(digest, label: str, value: bytes) -> None:
|
|
digest.update(f"{label}:{len(value)}\0".encode())
|
|
digest.update(value)
|
|
digest.update(b"\0")
|
|
|
|
|
|
def _package_sha256(package_directory: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
digest.update(HASH_DOMAIN)
|
|
files = sorted(
|
|
(path for path in package_directory.rglob("*") if path.is_file()),
|
|
key=lambda path: path.relative_to(package_directory).as_posix(),
|
|
)
|
|
assert files
|
|
for path in files:
|
|
assert not path.is_symlink()
|
|
relative = path.relative_to(package_directory).as_posix().encode("utf-8")
|
|
executable = b"1" if path.stat().st_mode & 0o111 else b"0"
|
|
_update_length_prefixed(digest, "path", relative)
|
|
_update_length_prefixed(digest, "executable", executable)
|
|
_update_length_prefixed(digest, "bytes", path.read_bytes())
|
|
return digest.hexdigest()
|
|
|
|
|
|
def test_personal_report_skill_is_versioned_and_registry_hash_matches_package_bytes() -> None:
|
|
registry = json.loads(REGISTRY.read_text(encoding="utf-8"))
|
|
matches = [
|
|
package
|
|
for package in registry["packages"]
|
|
if package["name"] == "jyotish-personal-report" and package["status"] == "active"
|
|
]
|
|
assert matches == [{
|
|
"name": "jyotish-personal-report",
|
|
"version": ACTIVE_VERSION,
|
|
"sha256": _package_sha256(VERSION_ROOT),
|
|
"sourceCommit": None,
|
|
"packagePath": f"skills/jyotish-personal-report/versions/{ACTIVE_VERSION}",
|
|
"status": "active",
|
|
}]
|
|
assert (PACKAGE_ROOT / "SKILL.md").read_bytes() == (VERSION_ROOT / "SKILL.md").read_bytes()
|
|
assert (PACKAGE_ROOT / "references/report-contract.md").read_bytes() == (
|
|
VERSION_ROOT / "references/report-contract.md"
|
|
).read_bytes()
|
|
assert not (VERSION_ROOT / "scripts").exists()
|
|
|
|
|
|
def test_personal_report_skill_encodes_evidence_only_two_stage_and_safety_boundaries() -> None:
|
|
skill = (VERSION_ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
contract = (VERSION_ROOT / "references/report-contract.md").read_text(encoding="utf-8")
|
|
combined = f"{skill}\n{contract}"
|
|
|
|
for required in (
|
|
"ReportEvidenceBundleV2",
|
|
"claimCards",
|
|
"blockedSections",
|
|
"planner → writer",
|
|
"concise",
|
|
"standard",
|
|
"deep",
|
|
"research",
|
|
"requestedThemes",
|
|
"聊天历史",
|
|
"工具轨迹",
|
|
"内部文件路径",
|
|
"禁止 HTML",
|
|
"不得计算、外推、缩窄、扩展或编造日期",
|
|
"禁止医疗诊断",
|
|
"禁止确定性财务承诺",
|
|
):
|
|
assert required in combined
|
|
|
|
assert "每个 requested theme" in skill
|
|
assert "恰好" in skill
|
|
assert "不得从宫位、星座、度数、分盘或 Dasha 自行推导含义" in skill
|
|
assert "research` 不代表可以联网或补证" in skill
|
|
# v1.1.0: the interpretive layer and the static packs are fact-bounded.
|
|
assert "interpretiveFacts" in skill
|
|
assert "themeNarrativeSeeds" in skill
|
|
assert "不得据此产生该 Claim Card 之外的新占星断言" in skill
|
|
assert "它**不是事实来源**" in skill
|