#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" export PYTHONPATH="$ROOT/src${PYTHONPATH:+:$PYTHONPATH}" VERIFY_DIR="$ROOT/.verify" CORPUS="$ROOT/examples/corpus/llm-wiki-mini" GOLDEN="$ROOT/examples/golden/application-core-spring-di-boundary.md" APP_BRIEF="$ROOT/examples/briefs/application-core-spring-di-blog.json" DEMO_DIR="$ROOT/examples/output/retry-policy-demo" rm -rf "$VERIFY_DIR" mkdir -p "$VERIFY_DIR" printf '\n== Unit and integration tests ==\n' if python3 -c 'import coverage' >/dev/null 2>&1; then python3 -m coverage erase python3 -m coverage run --source="$ROOT/src/claridoc" -m unittest discover -s "$ROOT/tests" -v python3 -m coverage report -m | tee "$VERIFY_DIR/coverage.txt" else python3 -m unittest discover -s "$ROOT/tests" -v printf 'coverage package unavailable; coverage report skipped\n' | tee "$VERIFY_DIR/coverage.txt" fi printf '\n== Contract and local-corpus validation ==\n' python3 -m claridoc validate \ --brief "$ROOT/examples/briefs/retry-policy-blog.json" \ --sources "$ROOT/examples/sources/retry-policy-sources.json" python3 -m claridoc collect \ --root "$CORPUS" \ --query 'application-core Spring DI 선택 이유 대안 비용 가드레일' \ --query 'TransactionPort spring-tx 금지 ArchUnit 검증' \ --top-k 24 \ --output "$VERIFY_DIR/application-core-sources.json" python3 -m claridoc validate \ --brief "$APP_BRIEF" \ --source-root "$CORPUS" python3 -m claridoc outline \ --brief "$APP_BRIEF" \ --source-root "$CORPUS" \ --output "$VERIFY_DIR/application-core-outline.json" python3 -m claridoc lint "$GOLDEN" \ --brief "$APP_BRIEF" \ --source-root "$CORPUS" \ --json \ --output "$VERIFY_DIR/application-core-golden-lint.json" printf '\n== Offline end-to-end pipeline ==\n' bash "$ROOT/scripts/run-demo.sh" printf '\n== Static, schema, provenance, and leakage checks ==\n' python3 - "$ROOT" "$VERIFY_DIR" <<'PY' from __future__ import annotations import ast import hashlib import json import re import sys from pathlib import Path root = Path(sys.argv[1]).resolve() verify_dir = Path(sys.argv[2]).resolve() python_files = sorted((root / "src").rglob("*.py")) + sorted((root / "tests").rglob("*.py")) for path in python_files: ast.parse(path.read_text(encoding="utf-8"), filename=str(path), feature_version=(3, 10)) ignored_parts = {"build", "dist", "__pycache__", ".git", ".verify"} json_files = [ path for path in sorted(root.rglob("*.json")) if not any(part in ignored_parts for part in path.parts) ] for path in json_files: json.loads(path.read_text(encoding="utf-8")) schema_instances = 0 try: import jsonschema except ImportError as exc: # pragma: no cover - verification environment diagnostic raise SystemExit(f"jsonschema is required by scripts/verify.sh: {exc}") for schema_path in sorted((root / "schemas").glob("*.schema.json")): jsonschema.Draft202012Validator.check_schema(json.loads(schema_path.read_text(encoding="utf-8"))) pairs = [ ("schemas/brief.schema.json", "examples/briefs/retry-policy-blog.json"), ("schemas/brief.schema.json", "examples/briefs/application-core-spring-di-blog.json"), ("schemas/source-pack.schema.json", "examples/sources/retry-policy-sources.json"), ("schemas/source-pack.schema.json", ".verify/application-core-sources.json"), ("schemas/outline.schema.json", ".verify/application-core-outline.json"), ] for schema_rel, instance_rel in pairs: schema = json.loads((root / schema_rel).read_text(encoding="utf-8")) instance = json.loads((root / instance_rel).read_text(encoding="utf-8")) jsonschema.Draft202012Validator(schema).validate(instance) schema_instances += 1 link_pattern = re.compile(r"\[[^\]]*\]\(([^)]+)\)") local_links = 0 for path in sorted(root.rglob("*.md")): if any(part in ignored_parts for part in path.parts): continue for target in link_pattern.findall(path.read_text(encoding="utf-8")): target = target.strip().split("#", 1)[0] if not target or re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:", target): continue local_links += 1 resolved = (path.parent / target).resolve() if not resolved.exists(): raise SystemExit(f"broken local Markdown link: {path.relative_to(root)} -> {target}") collected = json.loads((verify_dir / "application-core-sources.json").read_text(encoding="utf-8")) sources = collected.get("sources", []) if not sources: raise SystemExit("local corpus collection produced no sources") first = sources[0] first_text = "\n".join(first.get("facts", [])) if "수동 등록" not in first_text or "D13" not in first_text: raise SystemExit("decision-rationale chunk did not rank first") if Path(first.get("path", "")).is_absolute() or "/home/" in json.dumps(collected, ensure_ascii=False): raise SystemExit("collected evidence leaked an absolute path") if not any(item.get("source_type") == "canonical-project" for item in sources): raise SystemExit("local corpus did not retrieve canonical current-state evidence") if not any("SLF4J" in "\n".join(item.get("facts", [])) and "설명하지 않는다" in "\n".join(item.get("facts", [])) for item in sources): raise SystemExit("negative evidence boundary for unsupported SLF4J rationale is missing") golden_lint = json.loads((verify_dir / "application-core-golden-lint.json").read_text(encoding="utf-8")) material = [item for item in golden_lint.get("issues", []) if item.get("severity") in {"blocker", "error"}] if material: raise SystemExit(f"golden example has material lint issues: {material}") reader_docs = [ root / "examples/golden/application-core-spring-di-boundary.md", root / "examples/output/retry-policy-demo/final/document.md", ] forbidden = { "internal source marker": re.compile(r"\[(?:S|L)[A-Za-z0-9_-]+\]"), "evidence-pack narration": re.compile(r"제공된\s*(?:근거|자료)|근거\s*팩|확인\s*대상으로\s*제시"), "access-date boilerplate": re.compile(r"예시는\s*20\d{2}-\d{2}-\d{2}\s*기준"), "repository path": re.compile(r"(?:raw/branch-notes/|wiki/projects/|repo:///|/home/[^\s`]+)"), } for path in reader_docs: text = path.read_text(encoding="utf-8") for label, pattern in forbidden.items(): if pattern.search(text): raise SystemExit(f"reader-facing leakage ({label}) in {path.relative_to(root)}") if "SLF4J" in (root / "examples/golden/application-core-spring-di-boundary.md").read_text(encoding="utf-8"): raise SystemExit("golden reader-facing example invented or exposed unsupported SLF4J rationale") run_dir = root / "examples/output/retry-policy-demo" run = json.loads((run_dir / "run.json").read_text(encoding="utf-8")) if not run.get("passed"): raise SystemExit("demo quality gate did not pass") if not any("synthetic" in warning for warning in run.get("warnings", [])): raise SystemExit("mock-run synthetic-score warning is missing") required_artifacts = {"document", "quality_report", "provenance", "evidence_map", "outline", "events"} if not required_artifacts.issubset(run.get("artifacts", {})): raise SystemExit("run.json is missing reader/provenance artifact separation") manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8")) manifest_paths = {item["path"] for item in manifest["files"]} for required in {"final/document.md", "final/provenance.md", "final/evidence-map.json", "final/quality-report.md"}: if required not in manifest_paths: raise SystemExit(f"manifest missing required artifact: {required}") for item in manifest["files"]: artifact = run_dir / item["path"] if artifact.stat().st_size != item["bytes"]: raise SystemExit(f"manifest size mismatch: {item['path']}") digest = hashlib.sha256(artifact.read_bytes()).hexdigest() if digest != item["sha256"]: raise SystemExit(f"manifest hash mismatch: {item['path']}") print( "STATIC VERIFIED: " f"{len(python_files)} Python files parse with Python 3.10 grammar; " f"{len(json_files)} JSON files parse; 5 schemas are valid and " f"{schema_instances} representative instances validate; " f"{local_links} local Markdown links resolve; corpus rationale ranking, " "reader/provenance separation, forbidden-phrase regression checks, and manifest hashes pass; " f"mock demo PASS at {run['final_score']:.1f}/100 (synthetic score)." ) PY printf '\n== Wheel build and clean-install smoke test ==\n' rm -rf "$ROOT/build" "$ROOT/dist" "$ROOT"/*.egg-info "$ROOT/src"/*.egg-info mkdir -p "$ROOT/dist" python3 -m pip wheel "$ROOT" \ --no-deps \ --no-build-isolation \ --wheel-dir "$ROOT/dist" \ >"$VERIFY_DIR/pip-wheel.log" WHEEL="$(find "$ROOT/dist" -maxdepth 1 -type f -name 'claridoc_harness-0.2.0-*.whl' -print -quit)" if [[ -z "$WHEEL" ]]; then echo "0.2.0 wheel was not produced" >&2 exit 1 fi ( cd "$ROOT/dist" sha256sum "$(basename "$WHEEL")" > SHA256SUMS ) SMOKE_DIR="$(mktemp -d)" trap 'rm -rf "$SMOKE_DIR"' EXIT python3 -m venv "$SMOKE_DIR/venv" env -u PYTHONPATH "$SMOKE_DIR/venv/bin/python" -m pip install --force-reinstall --no-deps "$WHEEL" >"$VERIFY_DIR/pip-install.log" "$SMOKE_DIR/venv/bin/claridoc" --version | tee "$VERIFY_DIR/installed-version.txt" "$SMOKE_DIR/venv/bin/claridoc" validate \ --brief "$APP_BRIEF" \ --source-root "$CORPUS" "$SMOKE_DIR/venv/bin/claridoc" collect \ --root "$CORPUS" \ --query 'application-core Spring DI 선택 이유' \ --top-k 8 \ --output "$SMOKE_DIR/installed-sources.json" "$SMOKE_DIR/venv/bin/claridoc" lint "$GOLDEN" \ --brief "$APP_BRIEF" \ --source-root "$CORPUS" \ --output "$SMOKE_DIR/installed-golden-lint.md" "$SMOKE_DIR/venv/bin/claridoc" run \ --brief "$ROOT/examples/briefs/retry-policy-blog.json" \ --sources "$ROOT/examples/sources/retry-policy-sources.json" \ --config "$ROOT/config/pipeline.mock.json" \ --output "$SMOKE_DIR/installed-demo" printf '\n== Provider availability diagnostic ==\n' set +e python3 -m claridoc doctor \ --config "$ROOT/config/pipeline.multi-agent.example.json" \ >"$VERIFY_DIR/doctor.txt" 2>&1 DOCTOR_STATUS=$? set -e cat "$VERIFY_DIR/doctor.txt" if [[ "$DOCTOR_STATUS" -ne 0 && "$DOCTOR_STATUS" -ne 3 ]]; then echo "doctor returned unexpected status: $DOCTOR_STATUS" >&2 exit "$DOCTOR_STATUS" fi printf '\nVERIFICATION COMPLETE\n' printf 'Wheel: %s\n' "$WHEEL" printf 'SHA-256: %s\n' "$(cut -d' ' -f1 "$ROOT/dist/SHA256SUMS")"