From 1ca1eceb0863e0b86e3576bd968e11a80b5281e3 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 01:48:29 -0400 Subject: [PATCH 01/20] Add 0.1.4 evidence calibration foundation --- .claude-plugin/marketplace.json | 2 +- ARCHITECTURE.md | 2 + CHANGELOG.md | 8 + EVALUATION.md | 8 + TESTING.md | 19 + adapters/claude/.claude-plugin/plugin.json | 2 +- adapters/claude/skills/docs/SKILL.md | 2 +- .../docs/scripts/_docs_checker/evidence.py | 674 +++++++ .../scripts/_docs_checker/init_adoption.py | 2 +- .../skills/docs/scripts/evidence_receipt.py | 117 ++ adapters/copilot/SKILL.md | 2 +- .../copilot/scripts/_docs_checker/evidence.py | 674 +++++++ .../scripts/_docs_checker/init_adoption.py | 2 +- adapters/copilot/scripts/evidence_receipt.py | 117 ++ adapters/cursor/SKILL.md | 2 +- .../cursor/scripts/_docs_checker/evidence.py | 674 +++++++ .../scripts/_docs_checker/init_adoption.py | 2 +- adapters/cursor/scripts/evidence_receipt.py | 117 ++ adapters/gemini/docs.md | 2 +- adapters/grok/SKILL.md | 2 +- .../grok/scripts/_docs_checker/evidence.py | 674 +++++++ .../scripts/_docs_checker/init_adoption.py | 2 +- adapters/grok/scripts/evidence_receipt.py | 117 ++ adapters/opencode/docs.md | 2 +- adapters/plugin/.codex-plugin/plugin.json | 2 +- adapters/plugin/skills/docs/SKILL.md | 2 +- .../docs/scripts/_docs_checker/evidence.py | 674 +++++++ .../scripts/_docs_checker/init_adoption.py | 2 +- .../skills/docs/scripts/evidence_receipt.py | 117 ++ adapters/web/docs-help.txt | 2 +- evals/docs-corpus-baseline-v1.json | 1625 +++++++++++++++++ evals/docs-corpus-v1.json | 78 + evals/dogfood/cline-0.1.3.json | 180 ++ skills/docs/SKILL.md | 2 +- skills/docs/scripts/_docs_checker/evidence.py | 674 +++++++ .../scripts/_docs_checker/init_adoption.py | 2 +- skills/docs/scripts/evidence_receipt.py | 117 ++ tests/test_adapters.py | 14 +- tests/test_claude_distribution.py | 4 +- tests/test_docs_checker_architecture.py | 2 + tests/test_docs_evidence.py | 304 +++ tests/test_docs_skill.py | 6 +- tools/build_adapters.py | 2 + tools/prepare_docs_corpus.py | 138 ++ tools/run_docs_corpus.py | 294 +++ 45 files changed, 7437 insertions(+), 29 deletions(-) create mode 100644 adapters/claude/skills/docs/scripts/_docs_checker/evidence.py create mode 100644 adapters/claude/skills/docs/scripts/evidence_receipt.py create mode 100644 adapters/copilot/scripts/_docs_checker/evidence.py create mode 100644 adapters/copilot/scripts/evidence_receipt.py create mode 100644 adapters/cursor/scripts/_docs_checker/evidence.py create mode 100644 adapters/cursor/scripts/evidence_receipt.py create mode 100644 adapters/grok/scripts/_docs_checker/evidence.py create mode 100644 adapters/grok/scripts/evidence_receipt.py create mode 100644 adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py create mode 100644 adapters/plugin/skills/docs/scripts/evidence_receipt.py create mode 100644 evals/docs-corpus-baseline-v1.json create mode 100644 evals/docs-corpus-v1.json create mode 100644 evals/dogfood/cline-0.1.3.json create mode 100644 skills/docs/scripts/_docs_checker/evidence.py create mode 100644 skills/docs/scripts/evidence_receipt.py create mode 100644 tests/test_docs_evidence.py create mode 100644 tools/prepare_docs_corpus.py create mode 100644 tools/run_docs_corpus.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9bd553a..3c38239 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,7 +23,7 @@ "name": "diataxis-docs", "repository": "https://github.com/Statusnone420/Skills", "source": "./adapters/claude", - "version": "0.1.3" + "version": "0.1.4" } ] } diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 935ca1b..5dfadd6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4,6 +4,8 @@ The canonical source is `skills/docs/SKILL.md`, routed to `references/commands.m `skills/docs/scripts/check.py` is an optional network-free orchestration façade over cohesive `_docs_checker` modules. The checker confines paths to the repository, rejects reparse points, treats `.md`, `.markdown`, and `.mdx` as one inert document policy, selects one bounded navigation surface, resolves the supported Mintlify provider contract from the manifest's actual parent, uses tracked authority when Git visibility exists, parses Markdown links and anchors, reports unreachable pages and duplicate titles, evaluates state-declared hash freshness and Trust coverage, and reports provenance-tagged map/current-state bytes. Malformed, unsafe, ambiguous, or unsupported provider surfaces are unmeasured without a fallback score. The deterministic surface result is shared by Map, Check, Doctor, Audit, and Init; Init binds provider facts, provider findings, and the authority digest into preview/apply revalidation for both Git and non-Git repositories. Semantic model findings remain a labeled ceiling above provider facts. The 16,384-byte value is only `provisional_target_bytes`, not an enforced budget, health input, or deletion mandate. +`_docs_checker/evidence.py` owns sanitized evidence receipt v1. It copies existing deterministic health output without recalculating it, preserves category raw/earned/available values and score gates, and keeps deterministic, semantic, unresolved, Doctor, write, Git, and unavailable evidence in separate typed lanes. `evidence_receipt.py` is a stdout-first façade. The corpus preparation tool writes only newly owned, ignored sparse checkouts; the separate corpus runner requires clean detached exact commits, reads configuration as bounded bytes, executes no documentation or provider code, and proves target Git state is unchanged. + Discovery owns bounded metadata and selection policy; `init_adoption.py` owns the deterministic, engine-owned first-run adoption (scope selection, corpus accounting, preview construction, and receipt binding); `paths.py` owns confinement, normalization, reparse checks, and prune primitives; `memory.py` is read-only inspection; `lifecycle.py` owns pure authorization/state policy; and `lifecycle_io.py` owns transactional filesystem I/O. The dependency direction is one-way and the CLI façade stays thin. The committed `.diataxis/` control plane stores normalized routes, stable findings, verified hashes, disposition identity, and event history—never document bodies, prompts, hidden reasoning, or local-only filenames. Every generated adapter packages the complete canonical checker resource tree. Generic web prompts are composed per command from a shared safety core, one selected command contract, and required supporting rules; they do not concatenate the entire playbook. The observed prompt range is 3,484–24,679 UTF-8 bytes, with a 40,000-byte generator regression guard selected after measurement and 15,321 bytes of headroom. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ddc55e..938aefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.4 — Evidence and calibration foundation (2026-07-17) + +- Added a strict sanitized evidence receipt that preserves category-level rubric inputs, score gates, deterministic findings, semantic findings, unresolved candidates, Doctor evidence, Git/write state, and explicit unavailable values. +- Preserved the sanitized 0.1.3 Cline dogfood baseline without accessing raw session data or inventing missing commit, category, treatment, or approval evidence. +- Added a six-repository, commit-pinned documentation corpus with non-destructive sparse acquisition and a read-only runner that records supported Mintlify measurements and inert unsupported-provider evidence. +- Added provider-neutral literal-H1 and frontmatter-title observations without changing rubric v2, `HEALTH_WEIGHTS`, useful-entry gating, or provider scoring. +- Kept external models and APIs optional: deterministic receipts and corpus evidence remain local, standard-library based, and network-free after explicit corpus acquisition. + ## 0.1.3 — Deterministic navigation surfaces (2026-07-17) - Unified `.md`, `.markdown`, and `.mdx` under one inert, vendor-neutral structural policy without executing MDX or component code. diff --git a/EVALUATION.md b/EVALUATION.md index 7b37f4f..71a859b 100644 --- a/EVALUATION.md +++ b/EVALUATION.md @@ -57,6 +57,14 @@ State-declared verified document/source routes use newline- and NFC-normalized S Route tests use generated one-change mutations, invariant checks, deterministic cases, and retained named regressions. This applies Hypothesis/property-based testing ideas without adding Hypothesis, copying its source, or adding any runtime dependency. +## Evidence receipt and documentation corpus v1 + +Evidence receipt v1 is a sanitized product-evidence contract, not a transcript format. It records repository and checker identity, selected-surface facts, category-level rubric evidence, score gates, deterministic findings, semantic findings, unresolved candidates, Doctor evidence, Git/write state, and a complete index of unavailable evidence. `completed` with zero semantic findings is distinct from `not_assessed`; unavailable values are null and never become a product failure or numeric zero. + +The calibration corpus pins Cline, Supabase, Docusaurus, Vite, uv, and Kubernetes Website to immutable commits. Explicit preparation creates sparse ignored checkouts without dependencies or site builds. The runner then requires each checkout to be clean, detached, and at the exact commit; hashes bounded configuration bytes and reads inert entry text; and verifies Git status is unchanged. Only the existing Mintlify contract produces a structural score. Custom MDX, Docusaurus, VitePress, MkDocs, and Hugo evidence remains deterministic but structurally `not_assessed` until an inert provider contract exists. + +Literal H1 and scalar frontmatter-title observations are collected for calibration but do not affect rubric v2. The corpus baseline is evidence for a later scoring decision, not permission to tune `HEALTH_WEIGHTS`, useful-entry gating, or provider behavior in this release. No external model or API is required; an optional semantic lane must record its evaluator and cannot calculate or change the deterministic score. + ## Shared-engine dogfood Fresh isolated Codex agents ran the same canonical skill against the same repository state on Windows 11 on 2026-07-11: diff --git a/TESTING.md b/TESTING.md index 0b8bc17..c7a1ef9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -15,6 +15,8 @@ flowchart LR J["Selected-surface engine"] --> K["Markdown map or bounded Mintlify provider"] K --> I L["Malformed or unsupported provider input"] --> M["Unmeasured, fail closed"] + N["Pinned corpus manifest"] --> O["Read-only corpus runner"] + O --> P["Sanitized evidence receipt v1"] ``` ## Commands @@ -40,6 +42,21 @@ python -B tools/run_tests.py list python -B tools/run_tests.py verify ``` +Validate the receipt and corpus harness with local fixtures: + +```text +python -B -m unittest -v tests.test_docs_evidence +``` + +Public corpus acquisition is explicit and writes only new ignored checkouts. The runner itself performs no acquisition or target writes: + +```text +python -B tools/prepare_docs_corpus.py --manifest evals/docs-corpus-v1.json +python -B tools/run_docs_corpus.py --manifest evals/docs-corpus-v1.json --output evals/docs-corpus-baseline-v1.json +``` + +Preparation never deletes, updates, or reuses an existing corpus directory. The runner requires the official remote, exact detached commit, clean before/after Git status, and every declared entry/configuration probe. It never installs dependencies, builds sites, or executes MDX, JSX, JavaScript, TypeScript, TOML, YAML, Hugo shortcodes, imports, expressions, or components. + The orchestrator prints each group start and finish, module/test progress from verbose `unittest`, elapsed time, and a heartbeat every 30 seconds while work is still running. `--heartbeat-seconds` changes that interval and `--failfast` stops at the first failure. ## WSL performance @@ -58,4 +75,6 @@ Run the Ubuntu proof from a Linux-native checkout under `$HOME`, not directly fr Provider regressions also prove that Map, Check, Doctor, Audit, and Init use the same selected-surface evidence, including root-manifest authority, root README score isolation, tracked Git visibility, provider findings, and authority-digest Init revalidation on Git and non-Git fixtures. Semantic candidates remain labeled and bounded. +Corpus regressions additionally prove exact pins, inert configuration probes, unsupported-provider `not_assessed` states, orientation evidence that does not affect scoring, and zero target-repository writes. Rubric v2 and its category weights remain the comparison baseline until a separate calibration change is justified. + No valid test may be skipped, deleted, or weakened to pass a gate. A completion claim requires fresh output, a reviewed diff, and explicit separation of change-caused failures from verified pre-existing failures. diff --git a/adapters/claude/.claude-plugin/plugin.json b/adapters/claude/.claude-plugin/plugin.json index 9d27007..b634c75 100644 --- a/adapters/claude/.claude-plugin/plugin.json +++ b/adapters/claude/.claude-plugin/plugin.json @@ -13,5 +13,5 @@ "license": "Apache-2.0", "name": "diataxis-docs", "repository": "https://github.com/Statusnone420/Skills", - "version": "0.1.3" + "version": "0.1.4" } diff --git a/adapters/claude/skills/docs/SKILL.md b/adapters/claude/skills/docs/SKILL.md index 0ae62ae..1535f0a 100644 --- a/adapters/claude/skills/docs/SKILL.md +++ b/adapters/claude/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.3" + version: "0.1.4" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py new file mode 100644 index 0000000..8c86091 --- /dev/null +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -0,0 +1,674 @@ +"""Versioned, sanitized product-evidence receipts. + +This module stores facts, explicit absence, and lane provenance. It never +accepts transcript-shaped data or calculates the deterministic health score. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Mapping, Sequence +from pathlib import Path + +from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars +from .paths import normalize_repo_relative, safe_path + + +EVIDENCE_RECEIPT_VERSION = 1 +EVIDENCE_STATES = frozenset({"completed", "not_assessed", "unavailable", "failed"}) +MAX_RECEIPT_BYTES = 512 * 1024 +MAX_TEXT_BYTES = 512 +MAX_FINDINGS = 10_000 + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$") +_FORBIDDEN_KEY = re.compile( + r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", + re.IGNORECASE, +) +_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_CATEGORY_RAW_FIELDS = { + "entry": ( + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "valid_navigation_routes", + "complete_single_document", + "useful_entry", + ), + "path_safety": ("safe", "maintained"), + "links": ("valid", "checked"), + "anchors": ("valid", "checked"), + "reachability": ("reachable", "maintained"), + "titles": ("usable_unique", "maintained"), +} +_RECEIPT_FIELDS = frozenset( + { + "schema_version", + "receipt_id", + "repository", + "checker", + "run", + "surface", + "counts", + "orientation", + "health", + "evidence", + "doctor", + "write_audit", + "git", + "unavailable_evidence", + } +) + + +def evidence_value(status, value=None): + """Create one explicit evidence value without treating absence as zero.""" + if status not in EVIDENCE_STATES: + raise ValueError("evidence status is invalid") + if status == "completed" and value is None: + raise ValueError("completed evidence requires a value") + if status != "completed" and value is not None: + raise ValueError("incomplete evidence value must be null") + return {"status": status, "value": value} + + +def _mapping(value, name): + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _sequence(value, name): + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise ValueError(f"{name} must be an array") + return value + + +def _exact_keys(value, expected, name): + value = _mapping(value, name) + if set(value) != set(expected): + raise ValueError(f"{name} fields are invalid") + return value + + +def _bounded_text(value, name, *, pattern=None, allow_empty=False): + if not isinstance(value, str) or (not value and not allow_empty): + raise ValueError(f"{name} must be text") + if len(value.encode("utf-8", "strict")) > MAX_TEXT_BYTES: + raise ValueError(f"{name} exceeds capacity") + if any(ord(char) < 32 for char in value): + raise ValueError(f"{name} contains control characters") + if _WINDOWS_ABSOLUTE.match(value) or value.startswith(("/home/", "/Users/")): + raise ValueError(f"{name} exposes an absolute path") + if pattern is not None and pattern.fullmatch(value) is None: + raise ValueError(f"{name} is invalid") + return value + + +def _safe_identifier(value, name): + return _bounded_text(value, name, pattern=_SAFE_ID) + + +def _nonnegative_number(value, name): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be numeric") + if not math.isfinite(value) or value < 0: + raise ValueError(f"{name} must be finite and nonnegative") + return value + + +def _evidence(value, name, *, validator=None): + value = _exact_keys(value, {"status", "value"}, name) + status = value["status"] + if status not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + current = value["value"] + if status == "completed": + if current is None: + raise ValueError(f"{name}.value is required") + if validator is not None: + validator(current, f"{name}.value") + elif current is not None: + raise ValueError(f"{name}.value must be null") + return {"status": status, "value": current} + + +def _integer(value, name): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{name} must be a nonnegative integer") + return value + + +def _boolean(value, name): + if not isinstance(value, bool): + raise ValueError(f"{name} must be boolean") + return value + + +def _sha(value, name): + if not isinstance(value, str) or _SHA.fullmatch(value) is None: + raise ValueError(f"{name} must be a lowercase full commit SHA") + return value + + +def _digest(value, name): + if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: + raise ValueError(f"{name} must be a SHA-256 digest") + return value + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return value + + +def _route(value, name): + value = _bounded_text(value, name) + if value.startswith(("http://", "https://", "file:")): + raise ValueError(f"{name} must be a local route") + return value + + +def _reject_forbidden_keys(value, name="receipt"): + if isinstance(value, Mapping): + for key, child in value.items(): + if not isinstance(key, str) or _FORBIDDEN_KEY.search(key): + raise ValueError(f"{name} contains a forbidden field") + _reject_forbidden_keys(child, f"{name}.{key}") + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + _reject_forbidden_keys(child, f"{name}[{index}]") + + +def _validate_run(value): + value = _exact_keys( + value, + {"id", "client", "model_provider", "model", "effort", "turns", "duration_seconds", "commands"}, + "run", + ) + for field in ("id", "client", "model_provider", "model", "effort"): + _safe_identifier(value[field], f"run.{field}") + _evidence(value["turns"], "run.turns", validator=_integer) + _evidence(value["duration_seconds"], "run.duration_seconds", validator=_nonnegative_number) + commands = _sequence(value["commands"], "run.commands") + if not commands or len(commands) > 32: + raise ValueError("run.commands count is invalid") + for index, command in enumerate(commands): + _safe_identifier(command, f"run.commands[{index}]") + + +def _validate_finding(value, name): + value = _exact_keys(value, {"kind", "path", "line", "target", "fingerprint"}, name) + _safe_identifier(value["kind"], f"{name}.kind") + _evidence(value["path"], f"{name}.path", validator=_relative) + _evidence(value["line"], f"{name}.line", validator=_integer) + _evidence(value["target"], f"{name}.target", validator=_route) + _evidence(value["fingerprint"], f"{name}.fingerprint", validator=_digest) + + +def _validate_findings(value, name): + findings = _sequence(value, name) + if len(findings) > MAX_FINDINGS: + raise ValueError(f"{name} exceeds capacity") + for index, finding in enumerate(findings): + _validate_finding(finding, f"{name}[{index}]") + + +def _validate_lane(value, name, *, semantic=False): + expected = {"status", "findings"} + if semantic: + expected.add("evaluator") + value = _exact_keys(value, expected, name) + if value["status"] not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + _validate_findings(value["findings"], f"{name}.findings") + if value["status"] != "completed" and value["findings"]: + raise ValueError(f"{name} cannot contain findings when incomplete") + if semantic: + evaluator = _exact_keys( + value["evaluator"], {"provider", "model", "version"}, f"{name}.evaluator" + ) + for field in evaluator: + _evidence( + evaluator[field], + f"{name}.evaluator.{field}", + validator=_safe_identifier, + ) + + +def _validate_unresolved(value): + value = _exact_keys(value, {"status", "candidates"}, "evidence.unresolved") + if value["status"] not in EVIDENCE_STATES: + raise ValueError("evidence.unresolved.status is invalid") + candidates = _sequence(value["candidates"], "evidence.unresolved.candidates") + if len(candidates) > 1_000: + raise ValueError("evidence.unresolved.candidates exceeds capacity") + for index, candidate in enumerate(candidates): + candidate = _exact_keys( + candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" + ) + _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") + if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: + raise ValueError("unresolved candidate status is invalid") + + +def _validate_categories(value): + value = _mapping(value, "health.categories") + if set(value) - set(_CATEGORY_RAW_FIELDS): + raise ValueError("health.categories contains an unknown category") + for category, fields in _CATEGORY_RAW_FIELDS.items(): + if category not in value: + continue + row = _exact_keys( + value[category], {"raw", "earned", "available"}, f"health.categories.{category}" + ) + raw = _exact_keys(row["raw"], fields, f"health.categories.{category}.raw") + for field, evidence in raw.items(): + validator = _boolean if field in { + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "complete_single_document", + "useful_entry", + } else _integer + _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) + _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) + _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + + +def _collect_unavailable(value, path=""): + results = [] + if isinstance(value, Mapping): + if set(value) == {"status", "value"} and value["status"] != "completed": + results.append(path) + else: + for key, child in value.items(): + if key == "unavailable_evidence": + continue + child_path = f"{path}.{key}" if path else key + results.extend(_collect_unavailable(child, child_path)) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + results.extend(_collect_unavailable(child, f"{path}[{index}]")) + return sorted(results) + + +def validate_evidence_receipt(value): + """Validate and return one exact schema-v1 sanitized receipt.""" + value = _exact_keys(value, _RECEIPT_FIELDS, "receipt") + _reject_forbidden_keys(value) + if value["schema_version"] != EVIDENCE_RECEIPT_VERSION: + raise ValueError("receipt schema_version is invalid") + _safe_identifier(value["receipt_id"], "receipt_id") + + repository = _exact_keys(value["repository"], {"identifier", "commit"}, "repository") + _safe_identifier(repository["identifier"], "repository.identifier") + _evidence(repository["commit"], "repository.commit", validator=_sha) + + checker = _exact_keys(value["checker"], {"name", "version"}, "checker") + _safe_identifier(checker["name"], "checker.name") + _safe_identifier(checker["version"], "checker.version") + _validate_run(value["run"]) + + surface = _exact_keys( + value["surface"], {"provider", "authority", "provider_root", "entry"}, "surface" + ) + _safe_identifier(surface["provider"], "surface.provider") + for field in ("authority", "provider_root", "entry"): + _evidence(surface[field], f"surface.{field}", validator=_relative) + + counts = _exact_keys( + value["counts"], + {"pages", "hidden_pages", "redirects", "links_checked", "links_valid", "anchors_checked", "anchors_valid"}, + "counts", + ) + for field, current in counts.items(): + _evidence(current, f"counts.{field}", validator=_integer) + + orientation = _exact_keys( + value["orientation"], + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + + health = _exact_keys( + value["health"], + {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, + "health", + ) + if health["status"] not in EVIDENCE_STATES: + raise ValueError("health.status is invalid") + _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) + for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health[field], f"health.{field}", validator=_nonnegative_number) + _validate_categories(health["categories"]) + gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") + for field in gates: + _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) + + evidence = _exact_keys( + value["evidence"], {"deterministic", "semantic", "unresolved"}, "evidence" + ) + _validate_lane(evidence["deterministic"], "evidence.deterministic") + _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) + _validate_unresolved(evidence["unresolved"]) + + doctor = _exact_keys( + value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + if doctor["status"] not in EVIDENCE_STATES: + raise ValueError("doctor.status is invalid") + _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + + write_audit = _exact_keys( + value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" + ) + if write_audit["status"] not in EVIDENCE_STATES: + raise ValueError("write_audit.status is invalid") + _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) + _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + + git = _exact_keys(value["git"], {"before", "after"}, "git") + for field in git: + _evidence( + git[field], + f"git.{field}", + validator=lambda current, name: _bounded_text( + current, name, pattern=re.compile(r"^(?:clean|dirty)$") + ), + ) + + unavailable = _sequence(value["unavailable_evidence"], "unavailable_evidence") + for index, field in enumerate(unavailable): + _bounded_text(field, f"unavailable_evidence[{index}]") + expected_unavailable = _collect_unavailable(value) + if list(unavailable) != expected_unavailable: + raise ValueError("unavailable_evidence does not match explicit evidence states") + + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + if len(encoded) > MAX_RECEIPT_BYTES: + raise ValueError("receipt exceeds capacity") + return value + + +def canonical_receipt_bytes(value): + validated = validate_evidence_receipt(value) + return ( + json.dumps( + validated, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None): + return { + "kind": kind, + "path": evidence_value("completed", path) if path is not None else evidence_value("unavailable"), + "line": evidence_value("completed", line) if line is not None else evidence_value("unavailable"), + "target": evidence_value("completed", target) if target is not None else evidence_value("unavailable"), + "fingerprint": evidence_value("completed", fingerprint) + if fingerprint is not None + else evidence_value("unavailable"), + } + + +def observe_entry_orientation(root, entry): + """Read bounded inert text evidence; never evaluate provider or MDX code.""" + if entry is None: + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + root = Path(root).absolute() + relative = normalize_repo_relative(entry, "entry") + path = safe_path(root / relative, root) + try: + if path.stat().st_size > 2 * 1024 * 1024: + raise ValueError("entry exceeds capacity") + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + lines = text.removeprefix("\ufeff").splitlines() + in_fence = False + literal_h1 = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(("```", "~~~")): + in_fence = not in_fence + continue + if not in_fence and re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + return { + "literal_h1": evidence_value("completed", literal_h1), + "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "provider_rendered_title": evidence_value("unavailable"), + } + + +def health_receipt(health): + """Copy existing score evidence without recalculation or reinterpretation.""" + if not isinstance(health, Mapping): + unavailable = evidence_value("not_assessed") + return { + "status": "not_assessed", + "rubric_version": unavailable.copy(), + "percentage": unavailable.copy(), + "earned_weight": unavailable.copy(), + "available_weight": unavailable.copy(), + "categories": {}, + "score_gates": { + "map_has_h1": unavailable.copy(), + "useful_entry": unavailable.copy(), + }, + } + categories = {} + for category, raw_fields in _CATEGORY_RAW_FIELDS.items(): + source = health.get("categories", {}).get(category) + if not isinstance(source, Mapping): + continue + raw = source.get("raw", {}) + categories[category] = { + "raw": { + field: evidence_value("completed", raw[field]) + if field in raw + else evidence_value("unavailable") + for field in raw_fields + }, + "earned": evidence_value("completed", source["earned"]), + "available": evidence_value("completed", source["available"]), + } + entry = health.get("categories", {}).get("entry", {}).get("raw", {}) + return { + "status": "completed", + "rubric_version": evidence_value("completed", health["rubric_version"]), + "percentage": evidence_value("completed", health["percentage"]), + "earned_weight": evidence_value("completed", health["earned_weight"]), + "available_weight": evidence_value("completed", health["available_weight"]), + "categories": categories, + "score_gates": { + "map_has_h1": evidence_value("completed", entry["map_has_h1"]) + if "map_has_h1" in entry + else evidence_value("unavailable"), + "useful_entry": evidence_value("completed", entry["useful_entry"]) + if "useful_entry" in entry + else evidence_value("unavailable"), + }, + } + + +def build_evidence_receipt( + *, + receipt_id, + repository_identifier, + commit, + checker_version, + run, + checker_payload, + orientation, + semantic, + unresolved=(), + doctor=None, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", +): + """Build one receipt from existing deterministic checker evidence.""" + checker_payload = _mapping(checker_payload, "checker payload") + navigation = checker_payload.get("navigation", {}) + health = health_receipt(checker_payload.get("health")) + measured = isinstance(checker_payload.get("health"), Mapping) + counts = { + "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "hidden_pages": len(navigation.get("hidden_pages", ())), + "redirects": len(navigation.get("redirects", ())), + } + categories = health.get("categories", {}) + for receipt_name, category, raw_name in ( + ("links_checked", "links", "checked"), + ("links_valid", "links", "valid"), + ("anchors_checked", "anchors", "checked"), + ("anchors_valid", "anchors", "valid"), + ): + source = categories.get(category, {}).get("raw", {}).get(raw_name) + counts[receipt_name] = source if source is not None else evidence_value("not_assessed") + for name in ("pages", "hidden_pages", "redirects"): + counts[name] = ( + evidence_value("completed", counts[name]) + if measured + else evidence_value("not_assessed") + ) + + findings = [] + for raw in checker_payload.get("findings", ()): + if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): + continue + path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + line = raw.get("line") if isinstance(raw.get("line"), int) else None + target = raw.get("target") if isinstance(raw.get("target"), str) else None + findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) + + semantic = _mapping(semantic, "semantic") + evaluator = semantic.get("evaluator", {}) + semantic_lane = { + "status": semantic["status"], + "evaluator": { + field: evaluator.get(field, evidence_value("not_assessed")) + for field in ("provider", "model", "version") + }, + "findings": list(semantic.get("findings", ())), + } + doctor = doctor or { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + receipt = { + "schema_version": EVIDENCE_RECEIPT_VERSION, + "receipt_id": receipt_id, + "repository": { + "identifier": repository_identifier, + "commit": evidence_value("completed", commit) + if commit is not None + else evidence_value("unavailable"), + }, + "checker": {"name": "diataxis-docs", "version": checker_version}, + "run": dict(run), + "surface": { + "provider": navigation.get("provider", "unknown"), + "authority": evidence_value("completed", navigation["authority"]) + if navigation.get("authority") is not None + else evidence_value("unavailable"), + "provider_root": evidence_value("completed", navigation["provider_root"]) + if navigation.get("provider_root") is not None + else evidence_value("unavailable"), + "entry": evidence_value("completed", navigation["entry"]) + if navigation.get("entry") is not None + else evidence_value("unavailable"), + }, + "counts": counts, + "orientation": dict(orientation), + "health": health, + "evidence": { + "deterministic": {"status": "completed", "findings": findings}, + "semantic": semantic_lane, + "unresolved": {"status": "completed", "candidates": list(unresolved)}, + }, + "doctor": dict(doctor), + "write_audit": { + "status": "completed", + "writes_attempted": evidence_value("completed", writes_attempted), + "writes_observed": evidence_value("completed", writes_observed), + }, + "git": { + "before": evidence_value("completed", git_before), + "after": evidence_value("completed", git_after), + }, + "unavailable_evidence": [], + } + receipt["unavailable_evidence"] = _collect_unavailable(receipt) + return validate_evidence_receipt(receipt) + + +def config_probe(path): + """Return bounded presence/digest evidence without parsing configuration code.""" + path = Path(path) + try: + size = path.stat().st_size + if size > 2 * 1024 * 1024: + raise ValueError("configuration exceeds capacity") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + except (OSError, ValueError): + return {"status": "unavailable", "sha256": None, "bytes": None} + return {"status": "completed", "sha256": f"sha256:{digest}", "bytes": size} + + +__all__ = ( + "EVIDENCE_RECEIPT_VERSION", + "EVIDENCE_STATES", + "MAX_RECEIPT_BYTES", + "build_evidence_receipt", + "canonical_receipt_bytes", + "config_probe", + "evidence_value", + "finding_receipt", + "health_receipt", + "observe_entry_orientation", + "validate_evidence_receipt", +) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py b/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py index ec07ef2..96ba6dd 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.3" +SKILL_VERSION = "0.1.4" def canonical_request_bytes(value): diff --git a/adapters/claude/skills/docs/scripts/evidence_receipt.py b/adapters/claude/skills/docs/scripts/evidence_receipt.py new file mode 100644 index 0000000..ce0fbb9 --- /dev/null +++ b/adapters/claude/skills/docs/scripts/evidence_receipt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Emit one sanitized Diataxis Docs evidence receipt to stdout.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from _docs_checker.evidence import ( + build_evidence_receipt, + canonical_receipt_bytes, + observe_entry_orientation, +) +from _docs_checker.health import health_summary +from _docs_checker.init_adoption import SKILL_VERSION +from check import check + + +_PARSER = argparse.ArgumentParser() +_PARSER.add_argument("root") +_PARSER.add_argument("--metadata-file", required=True) +_PARSER.add_argument("--map", default="docs/README.md") +_PARSER.add_argument("--scope", default="docs") + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError("repository Git evidence is unavailable") + return completed.stdout.strip() + + +def _status(root): + return "dirty" if _git(root, "status", "--porcelain=v1", "--untracked-files=all") else "clean" + + +def _metadata(path): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} + if not isinstance(value, dict) or set(value) != expected: + raise ValueError("metadata fields are invalid") + return value + + +def main(argv=None): + try: + namespace = _PARSER.parse_args(argv) + root = Path(namespace.root).absolute() + metadata = _metadata(namespace.metadata_file) + before_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + before = "dirty" if before_raw else "clean" + commit = _git(root, "rev-parse", "HEAD") + findings, _, measurements = check( + root, + map_path=namespace.map, + scope=namespace.scope, + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + health["surface"] = measurements["navigation"]["scope"] + health["provider"] = measurements["navigation"]["provider"] + after_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + after = "dirty" if after_raw else "clean" + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + receipt = build_evidence_receipt( + receipt_id=metadata["receipt_id"], + repository_identifier=metadata["repository_identifier"], + commit=commit, + checker_version=SKILL_VERSION, + run=metadata["run"], + checker_payload=payload, + orientation=observe_entry_orientation(root, measurements["navigation"].get("entry")), + semantic=metadata["semantic"], + unresolved=metadata["unresolved"], + doctor=metadata["doctor"], + writes_attempted=0, + writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + git_before=before, + git_after=after, + ) + sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print( + json.dumps( + {"status": "failed", "error": str(exc), "receipt": None}, + ensure_ascii=True, + ) + ) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/adapters/copilot/SKILL.md b/adapters/copilot/SKILL.md index 0ae62ae..1535f0a 100644 --- a/adapters/copilot/SKILL.md +++ b/adapters/copilot/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.3" + version: "0.1.4" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py new file mode 100644 index 0000000..8c86091 --- /dev/null +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -0,0 +1,674 @@ +"""Versioned, sanitized product-evidence receipts. + +This module stores facts, explicit absence, and lane provenance. It never +accepts transcript-shaped data or calculates the deterministic health score. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Mapping, Sequence +from pathlib import Path + +from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars +from .paths import normalize_repo_relative, safe_path + + +EVIDENCE_RECEIPT_VERSION = 1 +EVIDENCE_STATES = frozenset({"completed", "not_assessed", "unavailable", "failed"}) +MAX_RECEIPT_BYTES = 512 * 1024 +MAX_TEXT_BYTES = 512 +MAX_FINDINGS = 10_000 + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$") +_FORBIDDEN_KEY = re.compile( + r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", + re.IGNORECASE, +) +_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_CATEGORY_RAW_FIELDS = { + "entry": ( + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "valid_navigation_routes", + "complete_single_document", + "useful_entry", + ), + "path_safety": ("safe", "maintained"), + "links": ("valid", "checked"), + "anchors": ("valid", "checked"), + "reachability": ("reachable", "maintained"), + "titles": ("usable_unique", "maintained"), +} +_RECEIPT_FIELDS = frozenset( + { + "schema_version", + "receipt_id", + "repository", + "checker", + "run", + "surface", + "counts", + "orientation", + "health", + "evidence", + "doctor", + "write_audit", + "git", + "unavailable_evidence", + } +) + + +def evidence_value(status, value=None): + """Create one explicit evidence value without treating absence as zero.""" + if status not in EVIDENCE_STATES: + raise ValueError("evidence status is invalid") + if status == "completed" and value is None: + raise ValueError("completed evidence requires a value") + if status != "completed" and value is not None: + raise ValueError("incomplete evidence value must be null") + return {"status": status, "value": value} + + +def _mapping(value, name): + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _sequence(value, name): + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise ValueError(f"{name} must be an array") + return value + + +def _exact_keys(value, expected, name): + value = _mapping(value, name) + if set(value) != set(expected): + raise ValueError(f"{name} fields are invalid") + return value + + +def _bounded_text(value, name, *, pattern=None, allow_empty=False): + if not isinstance(value, str) or (not value and not allow_empty): + raise ValueError(f"{name} must be text") + if len(value.encode("utf-8", "strict")) > MAX_TEXT_BYTES: + raise ValueError(f"{name} exceeds capacity") + if any(ord(char) < 32 for char in value): + raise ValueError(f"{name} contains control characters") + if _WINDOWS_ABSOLUTE.match(value) or value.startswith(("/home/", "/Users/")): + raise ValueError(f"{name} exposes an absolute path") + if pattern is not None and pattern.fullmatch(value) is None: + raise ValueError(f"{name} is invalid") + return value + + +def _safe_identifier(value, name): + return _bounded_text(value, name, pattern=_SAFE_ID) + + +def _nonnegative_number(value, name): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be numeric") + if not math.isfinite(value) or value < 0: + raise ValueError(f"{name} must be finite and nonnegative") + return value + + +def _evidence(value, name, *, validator=None): + value = _exact_keys(value, {"status", "value"}, name) + status = value["status"] + if status not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + current = value["value"] + if status == "completed": + if current is None: + raise ValueError(f"{name}.value is required") + if validator is not None: + validator(current, f"{name}.value") + elif current is not None: + raise ValueError(f"{name}.value must be null") + return {"status": status, "value": current} + + +def _integer(value, name): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{name} must be a nonnegative integer") + return value + + +def _boolean(value, name): + if not isinstance(value, bool): + raise ValueError(f"{name} must be boolean") + return value + + +def _sha(value, name): + if not isinstance(value, str) or _SHA.fullmatch(value) is None: + raise ValueError(f"{name} must be a lowercase full commit SHA") + return value + + +def _digest(value, name): + if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: + raise ValueError(f"{name} must be a SHA-256 digest") + return value + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return value + + +def _route(value, name): + value = _bounded_text(value, name) + if value.startswith(("http://", "https://", "file:")): + raise ValueError(f"{name} must be a local route") + return value + + +def _reject_forbidden_keys(value, name="receipt"): + if isinstance(value, Mapping): + for key, child in value.items(): + if not isinstance(key, str) or _FORBIDDEN_KEY.search(key): + raise ValueError(f"{name} contains a forbidden field") + _reject_forbidden_keys(child, f"{name}.{key}") + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + _reject_forbidden_keys(child, f"{name}[{index}]") + + +def _validate_run(value): + value = _exact_keys( + value, + {"id", "client", "model_provider", "model", "effort", "turns", "duration_seconds", "commands"}, + "run", + ) + for field in ("id", "client", "model_provider", "model", "effort"): + _safe_identifier(value[field], f"run.{field}") + _evidence(value["turns"], "run.turns", validator=_integer) + _evidence(value["duration_seconds"], "run.duration_seconds", validator=_nonnegative_number) + commands = _sequence(value["commands"], "run.commands") + if not commands or len(commands) > 32: + raise ValueError("run.commands count is invalid") + for index, command in enumerate(commands): + _safe_identifier(command, f"run.commands[{index}]") + + +def _validate_finding(value, name): + value = _exact_keys(value, {"kind", "path", "line", "target", "fingerprint"}, name) + _safe_identifier(value["kind"], f"{name}.kind") + _evidence(value["path"], f"{name}.path", validator=_relative) + _evidence(value["line"], f"{name}.line", validator=_integer) + _evidence(value["target"], f"{name}.target", validator=_route) + _evidence(value["fingerprint"], f"{name}.fingerprint", validator=_digest) + + +def _validate_findings(value, name): + findings = _sequence(value, name) + if len(findings) > MAX_FINDINGS: + raise ValueError(f"{name} exceeds capacity") + for index, finding in enumerate(findings): + _validate_finding(finding, f"{name}[{index}]") + + +def _validate_lane(value, name, *, semantic=False): + expected = {"status", "findings"} + if semantic: + expected.add("evaluator") + value = _exact_keys(value, expected, name) + if value["status"] not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + _validate_findings(value["findings"], f"{name}.findings") + if value["status"] != "completed" and value["findings"]: + raise ValueError(f"{name} cannot contain findings when incomplete") + if semantic: + evaluator = _exact_keys( + value["evaluator"], {"provider", "model", "version"}, f"{name}.evaluator" + ) + for field in evaluator: + _evidence( + evaluator[field], + f"{name}.evaluator.{field}", + validator=_safe_identifier, + ) + + +def _validate_unresolved(value): + value = _exact_keys(value, {"status", "candidates"}, "evidence.unresolved") + if value["status"] not in EVIDENCE_STATES: + raise ValueError("evidence.unresolved.status is invalid") + candidates = _sequence(value["candidates"], "evidence.unresolved.candidates") + if len(candidates) > 1_000: + raise ValueError("evidence.unresolved.candidates exceeds capacity") + for index, candidate in enumerate(candidates): + candidate = _exact_keys( + candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" + ) + _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") + if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: + raise ValueError("unresolved candidate status is invalid") + + +def _validate_categories(value): + value = _mapping(value, "health.categories") + if set(value) - set(_CATEGORY_RAW_FIELDS): + raise ValueError("health.categories contains an unknown category") + for category, fields in _CATEGORY_RAW_FIELDS.items(): + if category not in value: + continue + row = _exact_keys( + value[category], {"raw", "earned", "available"}, f"health.categories.{category}" + ) + raw = _exact_keys(row["raw"], fields, f"health.categories.{category}.raw") + for field, evidence in raw.items(): + validator = _boolean if field in { + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "complete_single_document", + "useful_entry", + } else _integer + _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) + _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) + _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + + +def _collect_unavailable(value, path=""): + results = [] + if isinstance(value, Mapping): + if set(value) == {"status", "value"} and value["status"] != "completed": + results.append(path) + else: + for key, child in value.items(): + if key == "unavailable_evidence": + continue + child_path = f"{path}.{key}" if path else key + results.extend(_collect_unavailable(child, child_path)) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + results.extend(_collect_unavailable(child, f"{path}[{index}]")) + return sorted(results) + + +def validate_evidence_receipt(value): + """Validate and return one exact schema-v1 sanitized receipt.""" + value = _exact_keys(value, _RECEIPT_FIELDS, "receipt") + _reject_forbidden_keys(value) + if value["schema_version"] != EVIDENCE_RECEIPT_VERSION: + raise ValueError("receipt schema_version is invalid") + _safe_identifier(value["receipt_id"], "receipt_id") + + repository = _exact_keys(value["repository"], {"identifier", "commit"}, "repository") + _safe_identifier(repository["identifier"], "repository.identifier") + _evidence(repository["commit"], "repository.commit", validator=_sha) + + checker = _exact_keys(value["checker"], {"name", "version"}, "checker") + _safe_identifier(checker["name"], "checker.name") + _safe_identifier(checker["version"], "checker.version") + _validate_run(value["run"]) + + surface = _exact_keys( + value["surface"], {"provider", "authority", "provider_root", "entry"}, "surface" + ) + _safe_identifier(surface["provider"], "surface.provider") + for field in ("authority", "provider_root", "entry"): + _evidence(surface[field], f"surface.{field}", validator=_relative) + + counts = _exact_keys( + value["counts"], + {"pages", "hidden_pages", "redirects", "links_checked", "links_valid", "anchors_checked", "anchors_valid"}, + "counts", + ) + for field, current in counts.items(): + _evidence(current, f"counts.{field}", validator=_integer) + + orientation = _exact_keys( + value["orientation"], + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + + health = _exact_keys( + value["health"], + {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, + "health", + ) + if health["status"] not in EVIDENCE_STATES: + raise ValueError("health.status is invalid") + _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) + for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health[field], f"health.{field}", validator=_nonnegative_number) + _validate_categories(health["categories"]) + gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") + for field in gates: + _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) + + evidence = _exact_keys( + value["evidence"], {"deterministic", "semantic", "unresolved"}, "evidence" + ) + _validate_lane(evidence["deterministic"], "evidence.deterministic") + _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) + _validate_unresolved(evidence["unresolved"]) + + doctor = _exact_keys( + value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + if doctor["status"] not in EVIDENCE_STATES: + raise ValueError("doctor.status is invalid") + _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + + write_audit = _exact_keys( + value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" + ) + if write_audit["status"] not in EVIDENCE_STATES: + raise ValueError("write_audit.status is invalid") + _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) + _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + + git = _exact_keys(value["git"], {"before", "after"}, "git") + for field in git: + _evidence( + git[field], + f"git.{field}", + validator=lambda current, name: _bounded_text( + current, name, pattern=re.compile(r"^(?:clean|dirty)$") + ), + ) + + unavailable = _sequence(value["unavailable_evidence"], "unavailable_evidence") + for index, field in enumerate(unavailable): + _bounded_text(field, f"unavailable_evidence[{index}]") + expected_unavailable = _collect_unavailable(value) + if list(unavailable) != expected_unavailable: + raise ValueError("unavailable_evidence does not match explicit evidence states") + + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + if len(encoded) > MAX_RECEIPT_BYTES: + raise ValueError("receipt exceeds capacity") + return value + + +def canonical_receipt_bytes(value): + validated = validate_evidence_receipt(value) + return ( + json.dumps( + validated, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None): + return { + "kind": kind, + "path": evidence_value("completed", path) if path is not None else evidence_value("unavailable"), + "line": evidence_value("completed", line) if line is not None else evidence_value("unavailable"), + "target": evidence_value("completed", target) if target is not None else evidence_value("unavailable"), + "fingerprint": evidence_value("completed", fingerprint) + if fingerprint is not None + else evidence_value("unavailable"), + } + + +def observe_entry_orientation(root, entry): + """Read bounded inert text evidence; never evaluate provider or MDX code.""" + if entry is None: + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + root = Path(root).absolute() + relative = normalize_repo_relative(entry, "entry") + path = safe_path(root / relative, root) + try: + if path.stat().st_size > 2 * 1024 * 1024: + raise ValueError("entry exceeds capacity") + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + lines = text.removeprefix("\ufeff").splitlines() + in_fence = False + literal_h1 = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(("```", "~~~")): + in_fence = not in_fence + continue + if not in_fence and re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + return { + "literal_h1": evidence_value("completed", literal_h1), + "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "provider_rendered_title": evidence_value("unavailable"), + } + + +def health_receipt(health): + """Copy existing score evidence without recalculation or reinterpretation.""" + if not isinstance(health, Mapping): + unavailable = evidence_value("not_assessed") + return { + "status": "not_assessed", + "rubric_version": unavailable.copy(), + "percentage": unavailable.copy(), + "earned_weight": unavailable.copy(), + "available_weight": unavailable.copy(), + "categories": {}, + "score_gates": { + "map_has_h1": unavailable.copy(), + "useful_entry": unavailable.copy(), + }, + } + categories = {} + for category, raw_fields in _CATEGORY_RAW_FIELDS.items(): + source = health.get("categories", {}).get(category) + if not isinstance(source, Mapping): + continue + raw = source.get("raw", {}) + categories[category] = { + "raw": { + field: evidence_value("completed", raw[field]) + if field in raw + else evidence_value("unavailable") + for field in raw_fields + }, + "earned": evidence_value("completed", source["earned"]), + "available": evidence_value("completed", source["available"]), + } + entry = health.get("categories", {}).get("entry", {}).get("raw", {}) + return { + "status": "completed", + "rubric_version": evidence_value("completed", health["rubric_version"]), + "percentage": evidence_value("completed", health["percentage"]), + "earned_weight": evidence_value("completed", health["earned_weight"]), + "available_weight": evidence_value("completed", health["available_weight"]), + "categories": categories, + "score_gates": { + "map_has_h1": evidence_value("completed", entry["map_has_h1"]) + if "map_has_h1" in entry + else evidence_value("unavailable"), + "useful_entry": evidence_value("completed", entry["useful_entry"]) + if "useful_entry" in entry + else evidence_value("unavailable"), + }, + } + + +def build_evidence_receipt( + *, + receipt_id, + repository_identifier, + commit, + checker_version, + run, + checker_payload, + orientation, + semantic, + unresolved=(), + doctor=None, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", +): + """Build one receipt from existing deterministic checker evidence.""" + checker_payload = _mapping(checker_payload, "checker payload") + navigation = checker_payload.get("navigation", {}) + health = health_receipt(checker_payload.get("health")) + measured = isinstance(checker_payload.get("health"), Mapping) + counts = { + "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "hidden_pages": len(navigation.get("hidden_pages", ())), + "redirects": len(navigation.get("redirects", ())), + } + categories = health.get("categories", {}) + for receipt_name, category, raw_name in ( + ("links_checked", "links", "checked"), + ("links_valid", "links", "valid"), + ("anchors_checked", "anchors", "checked"), + ("anchors_valid", "anchors", "valid"), + ): + source = categories.get(category, {}).get("raw", {}).get(raw_name) + counts[receipt_name] = source if source is not None else evidence_value("not_assessed") + for name in ("pages", "hidden_pages", "redirects"): + counts[name] = ( + evidence_value("completed", counts[name]) + if measured + else evidence_value("not_assessed") + ) + + findings = [] + for raw in checker_payload.get("findings", ()): + if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): + continue + path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + line = raw.get("line") if isinstance(raw.get("line"), int) else None + target = raw.get("target") if isinstance(raw.get("target"), str) else None + findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) + + semantic = _mapping(semantic, "semantic") + evaluator = semantic.get("evaluator", {}) + semantic_lane = { + "status": semantic["status"], + "evaluator": { + field: evaluator.get(field, evidence_value("not_assessed")) + for field in ("provider", "model", "version") + }, + "findings": list(semantic.get("findings", ())), + } + doctor = doctor or { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + receipt = { + "schema_version": EVIDENCE_RECEIPT_VERSION, + "receipt_id": receipt_id, + "repository": { + "identifier": repository_identifier, + "commit": evidence_value("completed", commit) + if commit is not None + else evidence_value("unavailable"), + }, + "checker": {"name": "diataxis-docs", "version": checker_version}, + "run": dict(run), + "surface": { + "provider": navigation.get("provider", "unknown"), + "authority": evidence_value("completed", navigation["authority"]) + if navigation.get("authority") is not None + else evidence_value("unavailable"), + "provider_root": evidence_value("completed", navigation["provider_root"]) + if navigation.get("provider_root") is not None + else evidence_value("unavailable"), + "entry": evidence_value("completed", navigation["entry"]) + if navigation.get("entry") is not None + else evidence_value("unavailable"), + }, + "counts": counts, + "orientation": dict(orientation), + "health": health, + "evidence": { + "deterministic": {"status": "completed", "findings": findings}, + "semantic": semantic_lane, + "unresolved": {"status": "completed", "candidates": list(unresolved)}, + }, + "doctor": dict(doctor), + "write_audit": { + "status": "completed", + "writes_attempted": evidence_value("completed", writes_attempted), + "writes_observed": evidence_value("completed", writes_observed), + }, + "git": { + "before": evidence_value("completed", git_before), + "after": evidence_value("completed", git_after), + }, + "unavailable_evidence": [], + } + receipt["unavailable_evidence"] = _collect_unavailable(receipt) + return validate_evidence_receipt(receipt) + + +def config_probe(path): + """Return bounded presence/digest evidence without parsing configuration code.""" + path = Path(path) + try: + size = path.stat().st_size + if size > 2 * 1024 * 1024: + raise ValueError("configuration exceeds capacity") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + except (OSError, ValueError): + return {"status": "unavailable", "sha256": None, "bytes": None} + return {"status": "completed", "sha256": f"sha256:{digest}", "bytes": size} + + +__all__ = ( + "EVIDENCE_RECEIPT_VERSION", + "EVIDENCE_STATES", + "MAX_RECEIPT_BYTES", + "build_evidence_receipt", + "canonical_receipt_bytes", + "config_probe", + "evidence_value", + "finding_receipt", + "health_receipt", + "observe_entry_orientation", + "validate_evidence_receipt", +) diff --git a/adapters/copilot/scripts/_docs_checker/init_adoption.py b/adapters/copilot/scripts/_docs_checker/init_adoption.py index ec07ef2..96ba6dd 100644 --- a/adapters/copilot/scripts/_docs_checker/init_adoption.py +++ b/adapters/copilot/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.3" +SKILL_VERSION = "0.1.4" def canonical_request_bytes(value): diff --git a/adapters/copilot/scripts/evidence_receipt.py b/adapters/copilot/scripts/evidence_receipt.py new file mode 100644 index 0000000..ce0fbb9 --- /dev/null +++ b/adapters/copilot/scripts/evidence_receipt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Emit one sanitized Diataxis Docs evidence receipt to stdout.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from _docs_checker.evidence import ( + build_evidence_receipt, + canonical_receipt_bytes, + observe_entry_orientation, +) +from _docs_checker.health import health_summary +from _docs_checker.init_adoption import SKILL_VERSION +from check import check + + +_PARSER = argparse.ArgumentParser() +_PARSER.add_argument("root") +_PARSER.add_argument("--metadata-file", required=True) +_PARSER.add_argument("--map", default="docs/README.md") +_PARSER.add_argument("--scope", default="docs") + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError("repository Git evidence is unavailable") + return completed.stdout.strip() + + +def _status(root): + return "dirty" if _git(root, "status", "--porcelain=v1", "--untracked-files=all") else "clean" + + +def _metadata(path): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} + if not isinstance(value, dict) or set(value) != expected: + raise ValueError("metadata fields are invalid") + return value + + +def main(argv=None): + try: + namespace = _PARSER.parse_args(argv) + root = Path(namespace.root).absolute() + metadata = _metadata(namespace.metadata_file) + before_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + before = "dirty" if before_raw else "clean" + commit = _git(root, "rev-parse", "HEAD") + findings, _, measurements = check( + root, + map_path=namespace.map, + scope=namespace.scope, + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + health["surface"] = measurements["navigation"]["scope"] + health["provider"] = measurements["navigation"]["provider"] + after_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + after = "dirty" if after_raw else "clean" + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + receipt = build_evidence_receipt( + receipt_id=metadata["receipt_id"], + repository_identifier=metadata["repository_identifier"], + commit=commit, + checker_version=SKILL_VERSION, + run=metadata["run"], + checker_payload=payload, + orientation=observe_entry_orientation(root, measurements["navigation"].get("entry")), + semantic=metadata["semantic"], + unresolved=metadata["unresolved"], + doctor=metadata["doctor"], + writes_attempted=0, + writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + git_before=before, + git_after=after, + ) + sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print( + json.dumps( + {"status": "failed", "error": str(exc), "receipt": None}, + ensure_ascii=True, + ) + ) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/adapters/cursor/SKILL.md b/adapters/cursor/SKILL.md index 0ae62ae..1535f0a 100644 --- a/adapters/cursor/SKILL.md +++ b/adapters/cursor/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.3" + version: "0.1.4" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py new file mode 100644 index 0000000..8c86091 --- /dev/null +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -0,0 +1,674 @@ +"""Versioned, sanitized product-evidence receipts. + +This module stores facts, explicit absence, and lane provenance. It never +accepts transcript-shaped data or calculates the deterministic health score. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Mapping, Sequence +from pathlib import Path + +from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars +from .paths import normalize_repo_relative, safe_path + + +EVIDENCE_RECEIPT_VERSION = 1 +EVIDENCE_STATES = frozenset({"completed", "not_assessed", "unavailable", "failed"}) +MAX_RECEIPT_BYTES = 512 * 1024 +MAX_TEXT_BYTES = 512 +MAX_FINDINGS = 10_000 + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$") +_FORBIDDEN_KEY = re.compile( + r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", + re.IGNORECASE, +) +_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_CATEGORY_RAW_FIELDS = { + "entry": ( + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "valid_navigation_routes", + "complete_single_document", + "useful_entry", + ), + "path_safety": ("safe", "maintained"), + "links": ("valid", "checked"), + "anchors": ("valid", "checked"), + "reachability": ("reachable", "maintained"), + "titles": ("usable_unique", "maintained"), +} +_RECEIPT_FIELDS = frozenset( + { + "schema_version", + "receipt_id", + "repository", + "checker", + "run", + "surface", + "counts", + "orientation", + "health", + "evidence", + "doctor", + "write_audit", + "git", + "unavailable_evidence", + } +) + + +def evidence_value(status, value=None): + """Create one explicit evidence value without treating absence as zero.""" + if status not in EVIDENCE_STATES: + raise ValueError("evidence status is invalid") + if status == "completed" and value is None: + raise ValueError("completed evidence requires a value") + if status != "completed" and value is not None: + raise ValueError("incomplete evidence value must be null") + return {"status": status, "value": value} + + +def _mapping(value, name): + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _sequence(value, name): + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise ValueError(f"{name} must be an array") + return value + + +def _exact_keys(value, expected, name): + value = _mapping(value, name) + if set(value) != set(expected): + raise ValueError(f"{name} fields are invalid") + return value + + +def _bounded_text(value, name, *, pattern=None, allow_empty=False): + if not isinstance(value, str) or (not value and not allow_empty): + raise ValueError(f"{name} must be text") + if len(value.encode("utf-8", "strict")) > MAX_TEXT_BYTES: + raise ValueError(f"{name} exceeds capacity") + if any(ord(char) < 32 for char in value): + raise ValueError(f"{name} contains control characters") + if _WINDOWS_ABSOLUTE.match(value) or value.startswith(("/home/", "/Users/")): + raise ValueError(f"{name} exposes an absolute path") + if pattern is not None and pattern.fullmatch(value) is None: + raise ValueError(f"{name} is invalid") + return value + + +def _safe_identifier(value, name): + return _bounded_text(value, name, pattern=_SAFE_ID) + + +def _nonnegative_number(value, name): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be numeric") + if not math.isfinite(value) or value < 0: + raise ValueError(f"{name} must be finite and nonnegative") + return value + + +def _evidence(value, name, *, validator=None): + value = _exact_keys(value, {"status", "value"}, name) + status = value["status"] + if status not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + current = value["value"] + if status == "completed": + if current is None: + raise ValueError(f"{name}.value is required") + if validator is not None: + validator(current, f"{name}.value") + elif current is not None: + raise ValueError(f"{name}.value must be null") + return {"status": status, "value": current} + + +def _integer(value, name): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{name} must be a nonnegative integer") + return value + + +def _boolean(value, name): + if not isinstance(value, bool): + raise ValueError(f"{name} must be boolean") + return value + + +def _sha(value, name): + if not isinstance(value, str) or _SHA.fullmatch(value) is None: + raise ValueError(f"{name} must be a lowercase full commit SHA") + return value + + +def _digest(value, name): + if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: + raise ValueError(f"{name} must be a SHA-256 digest") + return value + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return value + + +def _route(value, name): + value = _bounded_text(value, name) + if value.startswith(("http://", "https://", "file:")): + raise ValueError(f"{name} must be a local route") + return value + + +def _reject_forbidden_keys(value, name="receipt"): + if isinstance(value, Mapping): + for key, child in value.items(): + if not isinstance(key, str) or _FORBIDDEN_KEY.search(key): + raise ValueError(f"{name} contains a forbidden field") + _reject_forbidden_keys(child, f"{name}.{key}") + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + _reject_forbidden_keys(child, f"{name}[{index}]") + + +def _validate_run(value): + value = _exact_keys( + value, + {"id", "client", "model_provider", "model", "effort", "turns", "duration_seconds", "commands"}, + "run", + ) + for field in ("id", "client", "model_provider", "model", "effort"): + _safe_identifier(value[field], f"run.{field}") + _evidence(value["turns"], "run.turns", validator=_integer) + _evidence(value["duration_seconds"], "run.duration_seconds", validator=_nonnegative_number) + commands = _sequence(value["commands"], "run.commands") + if not commands or len(commands) > 32: + raise ValueError("run.commands count is invalid") + for index, command in enumerate(commands): + _safe_identifier(command, f"run.commands[{index}]") + + +def _validate_finding(value, name): + value = _exact_keys(value, {"kind", "path", "line", "target", "fingerprint"}, name) + _safe_identifier(value["kind"], f"{name}.kind") + _evidence(value["path"], f"{name}.path", validator=_relative) + _evidence(value["line"], f"{name}.line", validator=_integer) + _evidence(value["target"], f"{name}.target", validator=_route) + _evidence(value["fingerprint"], f"{name}.fingerprint", validator=_digest) + + +def _validate_findings(value, name): + findings = _sequence(value, name) + if len(findings) > MAX_FINDINGS: + raise ValueError(f"{name} exceeds capacity") + for index, finding in enumerate(findings): + _validate_finding(finding, f"{name}[{index}]") + + +def _validate_lane(value, name, *, semantic=False): + expected = {"status", "findings"} + if semantic: + expected.add("evaluator") + value = _exact_keys(value, expected, name) + if value["status"] not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + _validate_findings(value["findings"], f"{name}.findings") + if value["status"] != "completed" and value["findings"]: + raise ValueError(f"{name} cannot contain findings when incomplete") + if semantic: + evaluator = _exact_keys( + value["evaluator"], {"provider", "model", "version"}, f"{name}.evaluator" + ) + for field in evaluator: + _evidence( + evaluator[field], + f"{name}.evaluator.{field}", + validator=_safe_identifier, + ) + + +def _validate_unresolved(value): + value = _exact_keys(value, {"status", "candidates"}, "evidence.unresolved") + if value["status"] not in EVIDENCE_STATES: + raise ValueError("evidence.unresolved.status is invalid") + candidates = _sequence(value["candidates"], "evidence.unresolved.candidates") + if len(candidates) > 1_000: + raise ValueError("evidence.unresolved.candidates exceeds capacity") + for index, candidate in enumerate(candidates): + candidate = _exact_keys( + candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" + ) + _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") + if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: + raise ValueError("unresolved candidate status is invalid") + + +def _validate_categories(value): + value = _mapping(value, "health.categories") + if set(value) - set(_CATEGORY_RAW_FIELDS): + raise ValueError("health.categories contains an unknown category") + for category, fields in _CATEGORY_RAW_FIELDS.items(): + if category not in value: + continue + row = _exact_keys( + value[category], {"raw", "earned", "available"}, f"health.categories.{category}" + ) + raw = _exact_keys(row["raw"], fields, f"health.categories.{category}.raw") + for field, evidence in raw.items(): + validator = _boolean if field in { + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "complete_single_document", + "useful_entry", + } else _integer + _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) + _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) + _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + + +def _collect_unavailable(value, path=""): + results = [] + if isinstance(value, Mapping): + if set(value) == {"status", "value"} and value["status"] != "completed": + results.append(path) + else: + for key, child in value.items(): + if key == "unavailable_evidence": + continue + child_path = f"{path}.{key}" if path else key + results.extend(_collect_unavailable(child, child_path)) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + results.extend(_collect_unavailable(child, f"{path}[{index}]")) + return sorted(results) + + +def validate_evidence_receipt(value): + """Validate and return one exact schema-v1 sanitized receipt.""" + value = _exact_keys(value, _RECEIPT_FIELDS, "receipt") + _reject_forbidden_keys(value) + if value["schema_version"] != EVIDENCE_RECEIPT_VERSION: + raise ValueError("receipt schema_version is invalid") + _safe_identifier(value["receipt_id"], "receipt_id") + + repository = _exact_keys(value["repository"], {"identifier", "commit"}, "repository") + _safe_identifier(repository["identifier"], "repository.identifier") + _evidence(repository["commit"], "repository.commit", validator=_sha) + + checker = _exact_keys(value["checker"], {"name", "version"}, "checker") + _safe_identifier(checker["name"], "checker.name") + _safe_identifier(checker["version"], "checker.version") + _validate_run(value["run"]) + + surface = _exact_keys( + value["surface"], {"provider", "authority", "provider_root", "entry"}, "surface" + ) + _safe_identifier(surface["provider"], "surface.provider") + for field in ("authority", "provider_root", "entry"): + _evidence(surface[field], f"surface.{field}", validator=_relative) + + counts = _exact_keys( + value["counts"], + {"pages", "hidden_pages", "redirects", "links_checked", "links_valid", "anchors_checked", "anchors_valid"}, + "counts", + ) + for field, current in counts.items(): + _evidence(current, f"counts.{field}", validator=_integer) + + orientation = _exact_keys( + value["orientation"], + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + + health = _exact_keys( + value["health"], + {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, + "health", + ) + if health["status"] not in EVIDENCE_STATES: + raise ValueError("health.status is invalid") + _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) + for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health[field], f"health.{field}", validator=_nonnegative_number) + _validate_categories(health["categories"]) + gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") + for field in gates: + _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) + + evidence = _exact_keys( + value["evidence"], {"deterministic", "semantic", "unresolved"}, "evidence" + ) + _validate_lane(evidence["deterministic"], "evidence.deterministic") + _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) + _validate_unresolved(evidence["unresolved"]) + + doctor = _exact_keys( + value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + if doctor["status"] not in EVIDENCE_STATES: + raise ValueError("doctor.status is invalid") + _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + + write_audit = _exact_keys( + value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" + ) + if write_audit["status"] not in EVIDENCE_STATES: + raise ValueError("write_audit.status is invalid") + _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) + _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + + git = _exact_keys(value["git"], {"before", "after"}, "git") + for field in git: + _evidence( + git[field], + f"git.{field}", + validator=lambda current, name: _bounded_text( + current, name, pattern=re.compile(r"^(?:clean|dirty)$") + ), + ) + + unavailable = _sequence(value["unavailable_evidence"], "unavailable_evidence") + for index, field in enumerate(unavailable): + _bounded_text(field, f"unavailable_evidence[{index}]") + expected_unavailable = _collect_unavailable(value) + if list(unavailable) != expected_unavailable: + raise ValueError("unavailable_evidence does not match explicit evidence states") + + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + if len(encoded) > MAX_RECEIPT_BYTES: + raise ValueError("receipt exceeds capacity") + return value + + +def canonical_receipt_bytes(value): + validated = validate_evidence_receipt(value) + return ( + json.dumps( + validated, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None): + return { + "kind": kind, + "path": evidence_value("completed", path) if path is not None else evidence_value("unavailable"), + "line": evidence_value("completed", line) if line is not None else evidence_value("unavailable"), + "target": evidence_value("completed", target) if target is not None else evidence_value("unavailable"), + "fingerprint": evidence_value("completed", fingerprint) + if fingerprint is not None + else evidence_value("unavailable"), + } + + +def observe_entry_orientation(root, entry): + """Read bounded inert text evidence; never evaluate provider or MDX code.""" + if entry is None: + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + root = Path(root).absolute() + relative = normalize_repo_relative(entry, "entry") + path = safe_path(root / relative, root) + try: + if path.stat().st_size > 2 * 1024 * 1024: + raise ValueError("entry exceeds capacity") + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + lines = text.removeprefix("\ufeff").splitlines() + in_fence = False + literal_h1 = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(("```", "~~~")): + in_fence = not in_fence + continue + if not in_fence and re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + return { + "literal_h1": evidence_value("completed", literal_h1), + "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "provider_rendered_title": evidence_value("unavailable"), + } + + +def health_receipt(health): + """Copy existing score evidence without recalculation or reinterpretation.""" + if not isinstance(health, Mapping): + unavailable = evidence_value("not_assessed") + return { + "status": "not_assessed", + "rubric_version": unavailable.copy(), + "percentage": unavailable.copy(), + "earned_weight": unavailable.copy(), + "available_weight": unavailable.copy(), + "categories": {}, + "score_gates": { + "map_has_h1": unavailable.copy(), + "useful_entry": unavailable.copy(), + }, + } + categories = {} + for category, raw_fields in _CATEGORY_RAW_FIELDS.items(): + source = health.get("categories", {}).get(category) + if not isinstance(source, Mapping): + continue + raw = source.get("raw", {}) + categories[category] = { + "raw": { + field: evidence_value("completed", raw[field]) + if field in raw + else evidence_value("unavailable") + for field in raw_fields + }, + "earned": evidence_value("completed", source["earned"]), + "available": evidence_value("completed", source["available"]), + } + entry = health.get("categories", {}).get("entry", {}).get("raw", {}) + return { + "status": "completed", + "rubric_version": evidence_value("completed", health["rubric_version"]), + "percentage": evidence_value("completed", health["percentage"]), + "earned_weight": evidence_value("completed", health["earned_weight"]), + "available_weight": evidence_value("completed", health["available_weight"]), + "categories": categories, + "score_gates": { + "map_has_h1": evidence_value("completed", entry["map_has_h1"]) + if "map_has_h1" in entry + else evidence_value("unavailable"), + "useful_entry": evidence_value("completed", entry["useful_entry"]) + if "useful_entry" in entry + else evidence_value("unavailable"), + }, + } + + +def build_evidence_receipt( + *, + receipt_id, + repository_identifier, + commit, + checker_version, + run, + checker_payload, + orientation, + semantic, + unresolved=(), + doctor=None, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", +): + """Build one receipt from existing deterministic checker evidence.""" + checker_payload = _mapping(checker_payload, "checker payload") + navigation = checker_payload.get("navigation", {}) + health = health_receipt(checker_payload.get("health")) + measured = isinstance(checker_payload.get("health"), Mapping) + counts = { + "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "hidden_pages": len(navigation.get("hidden_pages", ())), + "redirects": len(navigation.get("redirects", ())), + } + categories = health.get("categories", {}) + for receipt_name, category, raw_name in ( + ("links_checked", "links", "checked"), + ("links_valid", "links", "valid"), + ("anchors_checked", "anchors", "checked"), + ("anchors_valid", "anchors", "valid"), + ): + source = categories.get(category, {}).get("raw", {}).get(raw_name) + counts[receipt_name] = source if source is not None else evidence_value("not_assessed") + for name in ("pages", "hidden_pages", "redirects"): + counts[name] = ( + evidence_value("completed", counts[name]) + if measured + else evidence_value("not_assessed") + ) + + findings = [] + for raw in checker_payload.get("findings", ()): + if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): + continue + path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + line = raw.get("line") if isinstance(raw.get("line"), int) else None + target = raw.get("target") if isinstance(raw.get("target"), str) else None + findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) + + semantic = _mapping(semantic, "semantic") + evaluator = semantic.get("evaluator", {}) + semantic_lane = { + "status": semantic["status"], + "evaluator": { + field: evaluator.get(field, evidence_value("not_assessed")) + for field in ("provider", "model", "version") + }, + "findings": list(semantic.get("findings", ())), + } + doctor = doctor or { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + receipt = { + "schema_version": EVIDENCE_RECEIPT_VERSION, + "receipt_id": receipt_id, + "repository": { + "identifier": repository_identifier, + "commit": evidence_value("completed", commit) + if commit is not None + else evidence_value("unavailable"), + }, + "checker": {"name": "diataxis-docs", "version": checker_version}, + "run": dict(run), + "surface": { + "provider": navigation.get("provider", "unknown"), + "authority": evidence_value("completed", navigation["authority"]) + if navigation.get("authority") is not None + else evidence_value("unavailable"), + "provider_root": evidence_value("completed", navigation["provider_root"]) + if navigation.get("provider_root") is not None + else evidence_value("unavailable"), + "entry": evidence_value("completed", navigation["entry"]) + if navigation.get("entry") is not None + else evidence_value("unavailable"), + }, + "counts": counts, + "orientation": dict(orientation), + "health": health, + "evidence": { + "deterministic": {"status": "completed", "findings": findings}, + "semantic": semantic_lane, + "unresolved": {"status": "completed", "candidates": list(unresolved)}, + }, + "doctor": dict(doctor), + "write_audit": { + "status": "completed", + "writes_attempted": evidence_value("completed", writes_attempted), + "writes_observed": evidence_value("completed", writes_observed), + }, + "git": { + "before": evidence_value("completed", git_before), + "after": evidence_value("completed", git_after), + }, + "unavailable_evidence": [], + } + receipt["unavailable_evidence"] = _collect_unavailable(receipt) + return validate_evidence_receipt(receipt) + + +def config_probe(path): + """Return bounded presence/digest evidence without parsing configuration code.""" + path = Path(path) + try: + size = path.stat().st_size + if size > 2 * 1024 * 1024: + raise ValueError("configuration exceeds capacity") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + except (OSError, ValueError): + return {"status": "unavailable", "sha256": None, "bytes": None} + return {"status": "completed", "sha256": f"sha256:{digest}", "bytes": size} + + +__all__ = ( + "EVIDENCE_RECEIPT_VERSION", + "EVIDENCE_STATES", + "MAX_RECEIPT_BYTES", + "build_evidence_receipt", + "canonical_receipt_bytes", + "config_probe", + "evidence_value", + "finding_receipt", + "health_receipt", + "observe_entry_orientation", + "validate_evidence_receipt", +) diff --git a/adapters/cursor/scripts/_docs_checker/init_adoption.py b/adapters/cursor/scripts/_docs_checker/init_adoption.py index ec07ef2..96ba6dd 100644 --- a/adapters/cursor/scripts/_docs_checker/init_adoption.py +++ b/adapters/cursor/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.3" +SKILL_VERSION = "0.1.4" def canonical_request_bytes(value): diff --git a/adapters/cursor/scripts/evidence_receipt.py b/adapters/cursor/scripts/evidence_receipt.py new file mode 100644 index 0000000..ce0fbb9 --- /dev/null +++ b/adapters/cursor/scripts/evidence_receipt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Emit one sanitized Diataxis Docs evidence receipt to stdout.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from _docs_checker.evidence import ( + build_evidence_receipt, + canonical_receipt_bytes, + observe_entry_orientation, +) +from _docs_checker.health import health_summary +from _docs_checker.init_adoption import SKILL_VERSION +from check import check + + +_PARSER = argparse.ArgumentParser() +_PARSER.add_argument("root") +_PARSER.add_argument("--metadata-file", required=True) +_PARSER.add_argument("--map", default="docs/README.md") +_PARSER.add_argument("--scope", default="docs") + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError("repository Git evidence is unavailable") + return completed.stdout.strip() + + +def _status(root): + return "dirty" if _git(root, "status", "--porcelain=v1", "--untracked-files=all") else "clean" + + +def _metadata(path): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} + if not isinstance(value, dict) or set(value) != expected: + raise ValueError("metadata fields are invalid") + return value + + +def main(argv=None): + try: + namespace = _PARSER.parse_args(argv) + root = Path(namespace.root).absolute() + metadata = _metadata(namespace.metadata_file) + before_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + before = "dirty" if before_raw else "clean" + commit = _git(root, "rev-parse", "HEAD") + findings, _, measurements = check( + root, + map_path=namespace.map, + scope=namespace.scope, + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + health["surface"] = measurements["navigation"]["scope"] + health["provider"] = measurements["navigation"]["provider"] + after_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + after = "dirty" if after_raw else "clean" + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + receipt = build_evidence_receipt( + receipt_id=metadata["receipt_id"], + repository_identifier=metadata["repository_identifier"], + commit=commit, + checker_version=SKILL_VERSION, + run=metadata["run"], + checker_payload=payload, + orientation=observe_entry_orientation(root, measurements["navigation"].get("entry")), + semantic=metadata["semantic"], + unresolved=metadata["unresolved"], + doctor=metadata["doctor"], + writes_attempted=0, + writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + git_before=before, + git_after=after, + ) + sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print( + json.dumps( + {"status": "failed", "error": str(exc), "receipt": None}, + ensure_ascii=True, + ) + ) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/adapters/gemini/docs.md b/adapters/gemini/docs.md index e9759a5..660e6f9 100644 --- a/adapters/gemini/docs.md +++ b/adapters/gemini/docs.md @@ -1,6 +1,6 @@ # /docs wrapper -Diátaxis Docs v0.1.3 +Diátaxis Docs v0.1.4 Instruction-enforced invocation: activate the shared `docs` skill explicitly, then parse one command and forward the raw trailing text verbatim (without shell interpolation). Usage: `/docs [raw trailing text]`. diff --git a/adapters/grok/SKILL.md b/adapters/grok/SKILL.md index 0ae62ae..1535f0a 100644 --- a/adapters/grok/SKILL.md +++ b/adapters/grok/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.3" + version: "0.1.4" user-invocable: true disable-model-invocation: true --- diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py new file mode 100644 index 0000000..8c86091 --- /dev/null +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -0,0 +1,674 @@ +"""Versioned, sanitized product-evidence receipts. + +This module stores facts, explicit absence, and lane provenance. It never +accepts transcript-shaped data or calculates the deterministic health score. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Mapping, Sequence +from pathlib import Path + +from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars +from .paths import normalize_repo_relative, safe_path + + +EVIDENCE_RECEIPT_VERSION = 1 +EVIDENCE_STATES = frozenset({"completed", "not_assessed", "unavailable", "failed"}) +MAX_RECEIPT_BYTES = 512 * 1024 +MAX_TEXT_BYTES = 512 +MAX_FINDINGS = 10_000 + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$") +_FORBIDDEN_KEY = re.compile( + r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", + re.IGNORECASE, +) +_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_CATEGORY_RAW_FIELDS = { + "entry": ( + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "valid_navigation_routes", + "complete_single_document", + "useful_entry", + ), + "path_safety": ("safe", "maintained"), + "links": ("valid", "checked"), + "anchors": ("valid", "checked"), + "reachability": ("reachable", "maintained"), + "titles": ("usable_unique", "maintained"), +} +_RECEIPT_FIELDS = frozenset( + { + "schema_version", + "receipt_id", + "repository", + "checker", + "run", + "surface", + "counts", + "orientation", + "health", + "evidence", + "doctor", + "write_audit", + "git", + "unavailable_evidence", + } +) + + +def evidence_value(status, value=None): + """Create one explicit evidence value without treating absence as zero.""" + if status not in EVIDENCE_STATES: + raise ValueError("evidence status is invalid") + if status == "completed" and value is None: + raise ValueError("completed evidence requires a value") + if status != "completed" and value is not None: + raise ValueError("incomplete evidence value must be null") + return {"status": status, "value": value} + + +def _mapping(value, name): + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _sequence(value, name): + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise ValueError(f"{name} must be an array") + return value + + +def _exact_keys(value, expected, name): + value = _mapping(value, name) + if set(value) != set(expected): + raise ValueError(f"{name} fields are invalid") + return value + + +def _bounded_text(value, name, *, pattern=None, allow_empty=False): + if not isinstance(value, str) or (not value and not allow_empty): + raise ValueError(f"{name} must be text") + if len(value.encode("utf-8", "strict")) > MAX_TEXT_BYTES: + raise ValueError(f"{name} exceeds capacity") + if any(ord(char) < 32 for char in value): + raise ValueError(f"{name} contains control characters") + if _WINDOWS_ABSOLUTE.match(value) or value.startswith(("/home/", "/Users/")): + raise ValueError(f"{name} exposes an absolute path") + if pattern is not None and pattern.fullmatch(value) is None: + raise ValueError(f"{name} is invalid") + return value + + +def _safe_identifier(value, name): + return _bounded_text(value, name, pattern=_SAFE_ID) + + +def _nonnegative_number(value, name): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be numeric") + if not math.isfinite(value) or value < 0: + raise ValueError(f"{name} must be finite and nonnegative") + return value + + +def _evidence(value, name, *, validator=None): + value = _exact_keys(value, {"status", "value"}, name) + status = value["status"] + if status not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + current = value["value"] + if status == "completed": + if current is None: + raise ValueError(f"{name}.value is required") + if validator is not None: + validator(current, f"{name}.value") + elif current is not None: + raise ValueError(f"{name}.value must be null") + return {"status": status, "value": current} + + +def _integer(value, name): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{name} must be a nonnegative integer") + return value + + +def _boolean(value, name): + if not isinstance(value, bool): + raise ValueError(f"{name} must be boolean") + return value + + +def _sha(value, name): + if not isinstance(value, str) or _SHA.fullmatch(value) is None: + raise ValueError(f"{name} must be a lowercase full commit SHA") + return value + + +def _digest(value, name): + if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: + raise ValueError(f"{name} must be a SHA-256 digest") + return value + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return value + + +def _route(value, name): + value = _bounded_text(value, name) + if value.startswith(("http://", "https://", "file:")): + raise ValueError(f"{name} must be a local route") + return value + + +def _reject_forbidden_keys(value, name="receipt"): + if isinstance(value, Mapping): + for key, child in value.items(): + if not isinstance(key, str) or _FORBIDDEN_KEY.search(key): + raise ValueError(f"{name} contains a forbidden field") + _reject_forbidden_keys(child, f"{name}.{key}") + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + _reject_forbidden_keys(child, f"{name}[{index}]") + + +def _validate_run(value): + value = _exact_keys( + value, + {"id", "client", "model_provider", "model", "effort", "turns", "duration_seconds", "commands"}, + "run", + ) + for field in ("id", "client", "model_provider", "model", "effort"): + _safe_identifier(value[field], f"run.{field}") + _evidence(value["turns"], "run.turns", validator=_integer) + _evidence(value["duration_seconds"], "run.duration_seconds", validator=_nonnegative_number) + commands = _sequence(value["commands"], "run.commands") + if not commands or len(commands) > 32: + raise ValueError("run.commands count is invalid") + for index, command in enumerate(commands): + _safe_identifier(command, f"run.commands[{index}]") + + +def _validate_finding(value, name): + value = _exact_keys(value, {"kind", "path", "line", "target", "fingerprint"}, name) + _safe_identifier(value["kind"], f"{name}.kind") + _evidence(value["path"], f"{name}.path", validator=_relative) + _evidence(value["line"], f"{name}.line", validator=_integer) + _evidence(value["target"], f"{name}.target", validator=_route) + _evidence(value["fingerprint"], f"{name}.fingerprint", validator=_digest) + + +def _validate_findings(value, name): + findings = _sequence(value, name) + if len(findings) > MAX_FINDINGS: + raise ValueError(f"{name} exceeds capacity") + for index, finding in enumerate(findings): + _validate_finding(finding, f"{name}[{index}]") + + +def _validate_lane(value, name, *, semantic=False): + expected = {"status", "findings"} + if semantic: + expected.add("evaluator") + value = _exact_keys(value, expected, name) + if value["status"] not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + _validate_findings(value["findings"], f"{name}.findings") + if value["status"] != "completed" and value["findings"]: + raise ValueError(f"{name} cannot contain findings when incomplete") + if semantic: + evaluator = _exact_keys( + value["evaluator"], {"provider", "model", "version"}, f"{name}.evaluator" + ) + for field in evaluator: + _evidence( + evaluator[field], + f"{name}.evaluator.{field}", + validator=_safe_identifier, + ) + + +def _validate_unresolved(value): + value = _exact_keys(value, {"status", "candidates"}, "evidence.unresolved") + if value["status"] not in EVIDENCE_STATES: + raise ValueError("evidence.unresolved.status is invalid") + candidates = _sequence(value["candidates"], "evidence.unresolved.candidates") + if len(candidates) > 1_000: + raise ValueError("evidence.unresolved.candidates exceeds capacity") + for index, candidate in enumerate(candidates): + candidate = _exact_keys( + candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" + ) + _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") + if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: + raise ValueError("unresolved candidate status is invalid") + + +def _validate_categories(value): + value = _mapping(value, "health.categories") + if set(value) - set(_CATEGORY_RAW_FIELDS): + raise ValueError("health.categories contains an unknown category") + for category, fields in _CATEGORY_RAW_FIELDS.items(): + if category not in value: + continue + row = _exact_keys( + value[category], {"raw", "earned", "available"}, f"health.categories.{category}" + ) + raw = _exact_keys(row["raw"], fields, f"health.categories.{category}.raw") + for field, evidence in raw.items(): + validator = _boolean if field in { + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "complete_single_document", + "useful_entry", + } else _integer + _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) + _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) + _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + + +def _collect_unavailable(value, path=""): + results = [] + if isinstance(value, Mapping): + if set(value) == {"status", "value"} and value["status"] != "completed": + results.append(path) + else: + for key, child in value.items(): + if key == "unavailable_evidence": + continue + child_path = f"{path}.{key}" if path else key + results.extend(_collect_unavailable(child, child_path)) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + results.extend(_collect_unavailable(child, f"{path}[{index}]")) + return sorted(results) + + +def validate_evidence_receipt(value): + """Validate and return one exact schema-v1 sanitized receipt.""" + value = _exact_keys(value, _RECEIPT_FIELDS, "receipt") + _reject_forbidden_keys(value) + if value["schema_version"] != EVIDENCE_RECEIPT_VERSION: + raise ValueError("receipt schema_version is invalid") + _safe_identifier(value["receipt_id"], "receipt_id") + + repository = _exact_keys(value["repository"], {"identifier", "commit"}, "repository") + _safe_identifier(repository["identifier"], "repository.identifier") + _evidence(repository["commit"], "repository.commit", validator=_sha) + + checker = _exact_keys(value["checker"], {"name", "version"}, "checker") + _safe_identifier(checker["name"], "checker.name") + _safe_identifier(checker["version"], "checker.version") + _validate_run(value["run"]) + + surface = _exact_keys( + value["surface"], {"provider", "authority", "provider_root", "entry"}, "surface" + ) + _safe_identifier(surface["provider"], "surface.provider") + for field in ("authority", "provider_root", "entry"): + _evidence(surface[field], f"surface.{field}", validator=_relative) + + counts = _exact_keys( + value["counts"], + {"pages", "hidden_pages", "redirects", "links_checked", "links_valid", "anchors_checked", "anchors_valid"}, + "counts", + ) + for field, current in counts.items(): + _evidence(current, f"counts.{field}", validator=_integer) + + orientation = _exact_keys( + value["orientation"], + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + + health = _exact_keys( + value["health"], + {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, + "health", + ) + if health["status"] not in EVIDENCE_STATES: + raise ValueError("health.status is invalid") + _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) + for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health[field], f"health.{field}", validator=_nonnegative_number) + _validate_categories(health["categories"]) + gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") + for field in gates: + _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) + + evidence = _exact_keys( + value["evidence"], {"deterministic", "semantic", "unresolved"}, "evidence" + ) + _validate_lane(evidence["deterministic"], "evidence.deterministic") + _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) + _validate_unresolved(evidence["unresolved"]) + + doctor = _exact_keys( + value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + if doctor["status"] not in EVIDENCE_STATES: + raise ValueError("doctor.status is invalid") + _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + + write_audit = _exact_keys( + value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" + ) + if write_audit["status"] not in EVIDENCE_STATES: + raise ValueError("write_audit.status is invalid") + _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) + _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + + git = _exact_keys(value["git"], {"before", "after"}, "git") + for field in git: + _evidence( + git[field], + f"git.{field}", + validator=lambda current, name: _bounded_text( + current, name, pattern=re.compile(r"^(?:clean|dirty)$") + ), + ) + + unavailable = _sequence(value["unavailable_evidence"], "unavailable_evidence") + for index, field in enumerate(unavailable): + _bounded_text(field, f"unavailable_evidence[{index}]") + expected_unavailable = _collect_unavailable(value) + if list(unavailable) != expected_unavailable: + raise ValueError("unavailable_evidence does not match explicit evidence states") + + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + if len(encoded) > MAX_RECEIPT_BYTES: + raise ValueError("receipt exceeds capacity") + return value + + +def canonical_receipt_bytes(value): + validated = validate_evidence_receipt(value) + return ( + json.dumps( + validated, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None): + return { + "kind": kind, + "path": evidence_value("completed", path) if path is not None else evidence_value("unavailable"), + "line": evidence_value("completed", line) if line is not None else evidence_value("unavailable"), + "target": evidence_value("completed", target) if target is not None else evidence_value("unavailable"), + "fingerprint": evidence_value("completed", fingerprint) + if fingerprint is not None + else evidence_value("unavailable"), + } + + +def observe_entry_orientation(root, entry): + """Read bounded inert text evidence; never evaluate provider or MDX code.""" + if entry is None: + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + root = Path(root).absolute() + relative = normalize_repo_relative(entry, "entry") + path = safe_path(root / relative, root) + try: + if path.stat().st_size > 2 * 1024 * 1024: + raise ValueError("entry exceeds capacity") + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + lines = text.removeprefix("\ufeff").splitlines() + in_fence = False + literal_h1 = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(("```", "~~~")): + in_fence = not in_fence + continue + if not in_fence and re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + return { + "literal_h1": evidence_value("completed", literal_h1), + "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "provider_rendered_title": evidence_value("unavailable"), + } + + +def health_receipt(health): + """Copy existing score evidence without recalculation or reinterpretation.""" + if not isinstance(health, Mapping): + unavailable = evidence_value("not_assessed") + return { + "status": "not_assessed", + "rubric_version": unavailable.copy(), + "percentage": unavailable.copy(), + "earned_weight": unavailable.copy(), + "available_weight": unavailable.copy(), + "categories": {}, + "score_gates": { + "map_has_h1": unavailable.copy(), + "useful_entry": unavailable.copy(), + }, + } + categories = {} + for category, raw_fields in _CATEGORY_RAW_FIELDS.items(): + source = health.get("categories", {}).get(category) + if not isinstance(source, Mapping): + continue + raw = source.get("raw", {}) + categories[category] = { + "raw": { + field: evidence_value("completed", raw[field]) + if field in raw + else evidence_value("unavailable") + for field in raw_fields + }, + "earned": evidence_value("completed", source["earned"]), + "available": evidence_value("completed", source["available"]), + } + entry = health.get("categories", {}).get("entry", {}).get("raw", {}) + return { + "status": "completed", + "rubric_version": evidence_value("completed", health["rubric_version"]), + "percentage": evidence_value("completed", health["percentage"]), + "earned_weight": evidence_value("completed", health["earned_weight"]), + "available_weight": evidence_value("completed", health["available_weight"]), + "categories": categories, + "score_gates": { + "map_has_h1": evidence_value("completed", entry["map_has_h1"]) + if "map_has_h1" in entry + else evidence_value("unavailable"), + "useful_entry": evidence_value("completed", entry["useful_entry"]) + if "useful_entry" in entry + else evidence_value("unavailable"), + }, + } + + +def build_evidence_receipt( + *, + receipt_id, + repository_identifier, + commit, + checker_version, + run, + checker_payload, + orientation, + semantic, + unresolved=(), + doctor=None, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", +): + """Build one receipt from existing deterministic checker evidence.""" + checker_payload = _mapping(checker_payload, "checker payload") + navigation = checker_payload.get("navigation", {}) + health = health_receipt(checker_payload.get("health")) + measured = isinstance(checker_payload.get("health"), Mapping) + counts = { + "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "hidden_pages": len(navigation.get("hidden_pages", ())), + "redirects": len(navigation.get("redirects", ())), + } + categories = health.get("categories", {}) + for receipt_name, category, raw_name in ( + ("links_checked", "links", "checked"), + ("links_valid", "links", "valid"), + ("anchors_checked", "anchors", "checked"), + ("anchors_valid", "anchors", "valid"), + ): + source = categories.get(category, {}).get("raw", {}).get(raw_name) + counts[receipt_name] = source if source is not None else evidence_value("not_assessed") + for name in ("pages", "hidden_pages", "redirects"): + counts[name] = ( + evidence_value("completed", counts[name]) + if measured + else evidence_value("not_assessed") + ) + + findings = [] + for raw in checker_payload.get("findings", ()): + if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): + continue + path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + line = raw.get("line") if isinstance(raw.get("line"), int) else None + target = raw.get("target") if isinstance(raw.get("target"), str) else None + findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) + + semantic = _mapping(semantic, "semantic") + evaluator = semantic.get("evaluator", {}) + semantic_lane = { + "status": semantic["status"], + "evaluator": { + field: evaluator.get(field, evidence_value("not_assessed")) + for field in ("provider", "model", "version") + }, + "findings": list(semantic.get("findings", ())), + } + doctor = doctor or { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + receipt = { + "schema_version": EVIDENCE_RECEIPT_VERSION, + "receipt_id": receipt_id, + "repository": { + "identifier": repository_identifier, + "commit": evidence_value("completed", commit) + if commit is not None + else evidence_value("unavailable"), + }, + "checker": {"name": "diataxis-docs", "version": checker_version}, + "run": dict(run), + "surface": { + "provider": navigation.get("provider", "unknown"), + "authority": evidence_value("completed", navigation["authority"]) + if navigation.get("authority") is not None + else evidence_value("unavailable"), + "provider_root": evidence_value("completed", navigation["provider_root"]) + if navigation.get("provider_root") is not None + else evidence_value("unavailable"), + "entry": evidence_value("completed", navigation["entry"]) + if navigation.get("entry") is not None + else evidence_value("unavailable"), + }, + "counts": counts, + "orientation": dict(orientation), + "health": health, + "evidence": { + "deterministic": {"status": "completed", "findings": findings}, + "semantic": semantic_lane, + "unresolved": {"status": "completed", "candidates": list(unresolved)}, + }, + "doctor": dict(doctor), + "write_audit": { + "status": "completed", + "writes_attempted": evidence_value("completed", writes_attempted), + "writes_observed": evidence_value("completed", writes_observed), + }, + "git": { + "before": evidence_value("completed", git_before), + "after": evidence_value("completed", git_after), + }, + "unavailable_evidence": [], + } + receipt["unavailable_evidence"] = _collect_unavailable(receipt) + return validate_evidence_receipt(receipt) + + +def config_probe(path): + """Return bounded presence/digest evidence without parsing configuration code.""" + path = Path(path) + try: + size = path.stat().st_size + if size > 2 * 1024 * 1024: + raise ValueError("configuration exceeds capacity") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + except (OSError, ValueError): + return {"status": "unavailable", "sha256": None, "bytes": None} + return {"status": "completed", "sha256": f"sha256:{digest}", "bytes": size} + + +__all__ = ( + "EVIDENCE_RECEIPT_VERSION", + "EVIDENCE_STATES", + "MAX_RECEIPT_BYTES", + "build_evidence_receipt", + "canonical_receipt_bytes", + "config_probe", + "evidence_value", + "finding_receipt", + "health_receipt", + "observe_entry_orientation", + "validate_evidence_receipt", +) diff --git a/adapters/grok/scripts/_docs_checker/init_adoption.py b/adapters/grok/scripts/_docs_checker/init_adoption.py index ec07ef2..96ba6dd 100644 --- a/adapters/grok/scripts/_docs_checker/init_adoption.py +++ b/adapters/grok/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.3" +SKILL_VERSION = "0.1.4" def canonical_request_bytes(value): diff --git a/adapters/grok/scripts/evidence_receipt.py b/adapters/grok/scripts/evidence_receipt.py new file mode 100644 index 0000000..ce0fbb9 --- /dev/null +++ b/adapters/grok/scripts/evidence_receipt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Emit one sanitized Diataxis Docs evidence receipt to stdout.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from _docs_checker.evidence import ( + build_evidence_receipt, + canonical_receipt_bytes, + observe_entry_orientation, +) +from _docs_checker.health import health_summary +from _docs_checker.init_adoption import SKILL_VERSION +from check import check + + +_PARSER = argparse.ArgumentParser() +_PARSER.add_argument("root") +_PARSER.add_argument("--metadata-file", required=True) +_PARSER.add_argument("--map", default="docs/README.md") +_PARSER.add_argument("--scope", default="docs") + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError("repository Git evidence is unavailable") + return completed.stdout.strip() + + +def _status(root): + return "dirty" if _git(root, "status", "--porcelain=v1", "--untracked-files=all") else "clean" + + +def _metadata(path): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} + if not isinstance(value, dict) or set(value) != expected: + raise ValueError("metadata fields are invalid") + return value + + +def main(argv=None): + try: + namespace = _PARSER.parse_args(argv) + root = Path(namespace.root).absolute() + metadata = _metadata(namespace.metadata_file) + before_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + before = "dirty" if before_raw else "clean" + commit = _git(root, "rev-parse", "HEAD") + findings, _, measurements = check( + root, + map_path=namespace.map, + scope=namespace.scope, + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + health["surface"] = measurements["navigation"]["scope"] + health["provider"] = measurements["navigation"]["provider"] + after_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + after = "dirty" if after_raw else "clean" + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + receipt = build_evidence_receipt( + receipt_id=metadata["receipt_id"], + repository_identifier=metadata["repository_identifier"], + commit=commit, + checker_version=SKILL_VERSION, + run=metadata["run"], + checker_payload=payload, + orientation=observe_entry_orientation(root, measurements["navigation"].get("entry")), + semantic=metadata["semantic"], + unresolved=metadata["unresolved"], + doctor=metadata["doctor"], + writes_attempted=0, + writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + git_before=before, + git_after=after, + ) + sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print( + json.dumps( + {"status": "failed", "error": str(exc), "receipt": None}, + ensure_ascii=True, + ) + ) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/adapters/opencode/docs.md b/adapters/opencode/docs.md index e9759a5..660e6f9 100644 --- a/adapters/opencode/docs.md +++ b/adapters/opencode/docs.md @@ -1,6 +1,6 @@ # /docs wrapper -Diátaxis Docs v0.1.3 +Diátaxis Docs v0.1.4 Instruction-enforced invocation: activate the shared `docs` skill explicitly, then parse one command and forward the raw trailing text verbatim (without shell interpolation). Usage: `/docs [raw trailing text]`. diff --git a/adapters/plugin/.codex-plugin/plugin.json b/adapters/plugin/.codex-plugin/plugin.json index bf697cd..b58e958 100644 --- a/adapters/plugin/.codex-plugin/plugin.json +++ b/adapters/plugin/.codex-plugin/plugin.json @@ -25,5 +25,5 @@ "name": "statusnone-skills", "repository": "https://github.com/Statusnone420/skills", "skills": "./skills/", - "version": "0.1.3" + "version": "0.1.4" } diff --git a/adapters/plugin/skills/docs/SKILL.md b/adapters/plugin/skills/docs/SKILL.md index 628596b..7a6cf74 100644 --- a/adapters/plugin/skills/docs/SKILL.md +++ b/adapters/plugin/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.3" + version: "0.1.4" --- # Diátaxis Docs diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py new file mode 100644 index 0000000..8c86091 --- /dev/null +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -0,0 +1,674 @@ +"""Versioned, sanitized product-evidence receipts. + +This module stores facts, explicit absence, and lane provenance. It never +accepts transcript-shaped data or calculates the deterministic health score. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Mapping, Sequence +from pathlib import Path + +from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars +from .paths import normalize_repo_relative, safe_path + + +EVIDENCE_RECEIPT_VERSION = 1 +EVIDENCE_STATES = frozenset({"completed", "not_assessed", "unavailable", "failed"}) +MAX_RECEIPT_BYTES = 512 * 1024 +MAX_TEXT_BYTES = 512 +MAX_FINDINGS = 10_000 + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$") +_FORBIDDEN_KEY = re.compile( + r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", + re.IGNORECASE, +) +_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_CATEGORY_RAW_FIELDS = { + "entry": ( + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "valid_navigation_routes", + "complete_single_document", + "useful_entry", + ), + "path_safety": ("safe", "maintained"), + "links": ("valid", "checked"), + "anchors": ("valid", "checked"), + "reachability": ("reachable", "maintained"), + "titles": ("usable_unique", "maintained"), +} +_RECEIPT_FIELDS = frozenset( + { + "schema_version", + "receipt_id", + "repository", + "checker", + "run", + "surface", + "counts", + "orientation", + "health", + "evidence", + "doctor", + "write_audit", + "git", + "unavailable_evidence", + } +) + + +def evidence_value(status, value=None): + """Create one explicit evidence value without treating absence as zero.""" + if status not in EVIDENCE_STATES: + raise ValueError("evidence status is invalid") + if status == "completed" and value is None: + raise ValueError("completed evidence requires a value") + if status != "completed" and value is not None: + raise ValueError("incomplete evidence value must be null") + return {"status": status, "value": value} + + +def _mapping(value, name): + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _sequence(value, name): + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise ValueError(f"{name} must be an array") + return value + + +def _exact_keys(value, expected, name): + value = _mapping(value, name) + if set(value) != set(expected): + raise ValueError(f"{name} fields are invalid") + return value + + +def _bounded_text(value, name, *, pattern=None, allow_empty=False): + if not isinstance(value, str) or (not value and not allow_empty): + raise ValueError(f"{name} must be text") + if len(value.encode("utf-8", "strict")) > MAX_TEXT_BYTES: + raise ValueError(f"{name} exceeds capacity") + if any(ord(char) < 32 for char in value): + raise ValueError(f"{name} contains control characters") + if _WINDOWS_ABSOLUTE.match(value) or value.startswith(("/home/", "/Users/")): + raise ValueError(f"{name} exposes an absolute path") + if pattern is not None and pattern.fullmatch(value) is None: + raise ValueError(f"{name} is invalid") + return value + + +def _safe_identifier(value, name): + return _bounded_text(value, name, pattern=_SAFE_ID) + + +def _nonnegative_number(value, name): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be numeric") + if not math.isfinite(value) or value < 0: + raise ValueError(f"{name} must be finite and nonnegative") + return value + + +def _evidence(value, name, *, validator=None): + value = _exact_keys(value, {"status", "value"}, name) + status = value["status"] + if status not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + current = value["value"] + if status == "completed": + if current is None: + raise ValueError(f"{name}.value is required") + if validator is not None: + validator(current, f"{name}.value") + elif current is not None: + raise ValueError(f"{name}.value must be null") + return {"status": status, "value": current} + + +def _integer(value, name): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{name} must be a nonnegative integer") + return value + + +def _boolean(value, name): + if not isinstance(value, bool): + raise ValueError(f"{name} must be boolean") + return value + + +def _sha(value, name): + if not isinstance(value, str) or _SHA.fullmatch(value) is None: + raise ValueError(f"{name} must be a lowercase full commit SHA") + return value + + +def _digest(value, name): + if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: + raise ValueError(f"{name} must be a SHA-256 digest") + return value + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return value + + +def _route(value, name): + value = _bounded_text(value, name) + if value.startswith(("http://", "https://", "file:")): + raise ValueError(f"{name} must be a local route") + return value + + +def _reject_forbidden_keys(value, name="receipt"): + if isinstance(value, Mapping): + for key, child in value.items(): + if not isinstance(key, str) or _FORBIDDEN_KEY.search(key): + raise ValueError(f"{name} contains a forbidden field") + _reject_forbidden_keys(child, f"{name}.{key}") + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + _reject_forbidden_keys(child, f"{name}[{index}]") + + +def _validate_run(value): + value = _exact_keys( + value, + {"id", "client", "model_provider", "model", "effort", "turns", "duration_seconds", "commands"}, + "run", + ) + for field in ("id", "client", "model_provider", "model", "effort"): + _safe_identifier(value[field], f"run.{field}") + _evidence(value["turns"], "run.turns", validator=_integer) + _evidence(value["duration_seconds"], "run.duration_seconds", validator=_nonnegative_number) + commands = _sequence(value["commands"], "run.commands") + if not commands or len(commands) > 32: + raise ValueError("run.commands count is invalid") + for index, command in enumerate(commands): + _safe_identifier(command, f"run.commands[{index}]") + + +def _validate_finding(value, name): + value = _exact_keys(value, {"kind", "path", "line", "target", "fingerprint"}, name) + _safe_identifier(value["kind"], f"{name}.kind") + _evidence(value["path"], f"{name}.path", validator=_relative) + _evidence(value["line"], f"{name}.line", validator=_integer) + _evidence(value["target"], f"{name}.target", validator=_route) + _evidence(value["fingerprint"], f"{name}.fingerprint", validator=_digest) + + +def _validate_findings(value, name): + findings = _sequence(value, name) + if len(findings) > MAX_FINDINGS: + raise ValueError(f"{name} exceeds capacity") + for index, finding in enumerate(findings): + _validate_finding(finding, f"{name}[{index}]") + + +def _validate_lane(value, name, *, semantic=False): + expected = {"status", "findings"} + if semantic: + expected.add("evaluator") + value = _exact_keys(value, expected, name) + if value["status"] not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + _validate_findings(value["findings"], f"{name}.findings") + if value["status"] != "completed" and value["findings"]: + raise ValueError(f"{name} cannot contain findings when incomplete") + if semantic: + evaluator = _exact_keys( + value["evaluator"], {"provider", "model", "version"}, f"{name}.evaluator" + ) + for field in evaluator: + _evidence( + evaluator[field], + f"{name}.evaluator.{field}", + validator=_safe_identifier, + ) + + +def _validate_unresolved(value): + value = _exact_keys(value, {"status", "candidates"}, "evidence.unresolved") + if value["status"] not in EVIDENCE_STATES: + raise ValueError("evidence.unresolved.status is invalid") + candidates = _sequence(value["candidates"], "evidence.unresolved.candidates") + if len(candidates) > 1_000: + raise ValueError("evidence.unresolved.candidates exceeds capacity") + for index, candidate in enumerate(candidates): + candidate = _exact_keys( + candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" + ) + _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") + if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: + raise ValueError("unresolved candidate status is invalid") + + +def _validate_categories(value): + value = _mapping(value, "health.categories") + if set(value) - set(_CATEGORY_RAW_FIELDS): + raise ValueError("health.categories contains an unknown category") + for category, fields in _CATEGORY_RAW_FIELDS.items(): + if category not in value: + continue + row = _exact_keys( + value[category], {"raw", "earned", "available"}, f"health.categories.{category}" + ) + raw = _exact_keys(row["raw"], fields, f"health.categories.{category}.raw") + for field, evidence in raw.items(): + validator = _boolean if field in { + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "complete_single_document", + "useful_entry", + } else _integer + _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) + _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) + _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + + +def _collect_unavailable(value, path=""): + results = [] + if isinstance(value, Mapping): + if set(value) == {"status", "value"} and value["status"] != "completed": + results.append(path) + else: + for key, child in value.items(): + if key == "unavailable_evidence": + continue + child_path = f"{path}.{key}" if path else key + results.extend(_collect_unavailable(child, child_path)) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + results.extend(_collect_unavailable(child, f"{path}[{index}]")) + return sorted(results) + + +def validate_evidence_receipt(value): + """Validate and return one exact schema-v1 sanitized receipt.""" + value = _exact_keys(value, _RECEIPT_FIELDS, "receipt") + _reject_forbidden_keys(value) + if value["schema_version"] != EVIDENCE_RECEIPT_VERSION: + raise ValueError("receipt schema_version is invalid") + _safe_identifier(value["receipt_id"], "receipt_id") + + repository = _exact_keys(value["repository"], {"identifier", "commit"}, "repository") + _safe_identifier(repository["identifier"], "repository.identifier") + _evidence(repository["commit"], "repository.commit", validator=_sha) + + checker = _exact_keys(value["checker"], {"name", "version"}, "checker") + _safe_identifier(checker["name"], "checker.name") + _safe_identifier(checker["version"], "checker.version") + _validate_run(value["run"]) + + surface = _exact_keys( + value["surface"], {"provider", "authority", "provider_root", "entry"}, "surface" + ) + _safe_identifier(surface["provider"], "surface.provider") + for field in ("authority", "provider_root", "entry"): + _evidence(surface[field], f"surface.{field}", validator=_relative) + + counts = _exact_keys( + value["counts"], + {"pages", "hidden_pages", "redirects", "links_checked", "links_valid", "anchors_checked", "anchors_valid"}, + "counts", + ) + for field, current in counts.items(): + _evidence(current, f"counts.{field}", validator=_integer) + + orientation = _exact_keys( + value["orientation"], + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + + health = _exact_keys( + value["health"], + {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, + "health", + ) + if health["status"] not in EVIDENCE_STATES: + raise ValueError("health.status is invalid") + _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) + for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health[field], f"health.{field}", validator=_nonnegative_number) + _validate_categories(health["categories"]) + gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") + for field in gates: + _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) + + evidence = _exact_keys( + value["evidence"], {"deterministic", "semantic", "unresolved"}, "evidence" + ) + _validate_lane(evidence["deterministic"], "evidence.deterministic") + _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) + _validate_unresolved(evidence["unresolved"]) + + doctor = _exact_keys( + value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + if doctor["status"] not in EVIDENCE_STATES: + raise ValueError("doctor.status is invalid") + _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + + write_audit = _exact_keys( + value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" + ) + if write_audit["status"] not in EVIDENCE_STATES: + raise ValueError("write_audit.status is invalid") + _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) + _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + + git = _exact_keys(value["git"], {"before", "after"}, "git") + for field in git: + _evidence( + git[field], + f"git.{field}", + validator=lambda current, name: _bounded_text( + current, name, pattern=re.compile(r"^(?:clean|dirty)$") + ), + ) + + unavailable = _sequence(value["unavailable_evidence"], "unavailable_evidence") + for index, field in enumerate(unavailable): + _bounded_text(field, f"unavailable_evidence[{index}]") + expected_unavailable = _collect_unavailable(value) + if list(unavailable) != expected_unavailable: + raise ValueError("unavailable_evidence does not match explicit evidence states") + + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + if len(encoded) > MAX_RECEIPT_BYTES: + raise ValueError("receipt exceeds capacity") + return value + + +def canonical_receipt_bytes(value): + validated = validate_evidence_receipt(value) + return ( + json.dumps( + validated, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None): + return { + "kind": kind, + "path": evidence_value("completed", path) if path is not None else evidence_value("unavailable"), + "line": evidence_value("completed", line) if line is not None else evidence_value("unavailable"), + "target": evidence_value("completed", target) if target is not None else evidence_value("unavailable"), + "fingerprint": evidence_value("completed", fingerprint) + if fingerprint is not None + else evidence_value("unavailable"), + } + + +def observe_entry_orientation(root, entry): + """Read bounded inert text evidence; never evaluate provider or MDX code.""" + if entry is None: + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + root = Path(root).absolute() + relative = normalize_repo_relative(entry, "entry") + path = safe_path(root / relative, root) + try: + if path.stat().st_size > 2 * 1024 * 1024: + raise ValueError("entry exceeds capacity") + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + lines = text.removeprefix("\ufeff").splitlines() + in_fence = False + literal_h1 = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(("```", "~~~")): + in_fence = not in_fence + continue + if not in_fence and re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + return { + "literal_h1": evidence_value("completed", literal_h1), + "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "provider_rendered_title": evidence_value("unavailable"), + } + + +def health_receipt(health): + """Copy existing score evidence without recalculation or reinterpretation.""" + if not isinstance(health, Mapping): + unavailable = evidence_value("not_assessed") + return { + "status": "not_assessed", + "rubric_version": unavailable.copy(), + "percentage": unavailable.copy(), + "earned_weight": unavailable.copy(), + "available_weight": unavailable.copy(), + "categories": {}, + "score_gates": { + "map_has_h1": unavailable.copy(), + "useful_entry": unavailable.copy(), + }, + } + categories = {} + for category, raw_fields in _CATEGORY_RAW_FIELDS.items(): + source = health.get("categories", {}).get(category) + if not isinstance(source, Mapping): + continue + raw = source.get("raw", {}) + categories[category] = { + "raw": { + field: evidence_value("completed", raw[field]) + if field in raw + else evidence_value("unavailable") + for field in raw_fields + }, + "earned": evidence_value("completed", source["earned"]), + "available": evidence_value("completed", source["available"]), + } + entry = health.get("categories", {}).get("entry", {}).get("raw", {}) + return { + "status": "completed", + "rubric_version": evidence_value("completed", health["rubric_version"]), + "percentage": evidence_value("completed", health["percentage"]), + "earned_weight": evidence_value("completed", health["earned_weight"]), + "available_weight": evidence_value("completed", health["available_weight"]), + "categories": categories, + "score_gates": { + "map_has_h1": evidence_value("completed", entry["map_has_h1"]) + if "map_has_h1" in entry + else evidence_value("unavailable"), + "useful_entry": evidence_value("completed", entry["useful_entry"]) + if "useful_entry" in entry + else evidence_value("unavailable"), + }, + } + + +def build_evidence_receipt( + *, + receipt_id, + repository_identifier, + commit, + checker_version, + run, + checker_payload, + orientation, + semantic, + unresolved=(), + doctor=None, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", +): + """Build one receipt from existing deterministic checker evidence.""" + checker_payload = _mapping(checker_payload, "checker payload") + navigation = checker_payload.get("navigation", {}) + health = health_receipt(checker_payload.get("health")) + measured = isinstance(checker_payload.get("health"), Mapping) + counts = { + "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "hidden_pages": len(navigation.get("hidden_pages", ())), + "redirects": len(navigation.get("redirects", ())), + } + categories = health.get("categories", {}) + for receipt_name, category, raw_name in ( + ("links_checked", "links", "checked"), + ("links_valid", "links", "valid"), + ("anchors_checked", "anchors", "checked"), + ("anchors_valid", "anchors", "valid"), + ): + source = categories.get(category, {}).get("raw", {}).get(raw_name) + counts[receipt_name] = source if source is not None else evidence_value("not_assessed") + for name in ("pages", "hidden_pages", "redirects"): + counts[name] = ( + evidence_value("completed", counts[name]) + if measured + else evidence_value("not_assessed") + ) + + findings = [] + for raw in checker_payload.get("findings", ()): + if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): + continue + path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + line = raw.get("line") if isinstance(raw.get("line"), int) else None + target = raw.get("target") if isinstance(raw.get("target"), str) else None + findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) + + semantic = _mapping(semantic, "semantic") + evaluator = semantic.get("evaluator", {}) + semantic_lane = { + "status": semantic["status"], + "evaluator": { + field: evaluator.get(field, evidence_value("not_assessed")) + for field in ("provider", "model", "version") + }, + "findings": list(semantic.get("findings", ())), + } + doctor = doctor or { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + receipt = { + "schema_version": EVIDENCE_RECEIPT_VERSION, + "receipt_id": receipt_id, + "repository": { + "identifier": repository_identifier, + "commit": evidence_value("completed", commit) + if commit is not None + else evidence_value("unavailable"), + }, + "checker": {"name": "diataxis-docs", "version": checker_version}, + "run": dict(run), + "surface": { + "provider": navigation.get("provider", "unknown"), + "authority": evidence_value("completed", navigation["authority"]) + if navigation.get("authority") is not None + else evidence_value("unavailable"), + "provider_root": evidence_value("completed", navigation["provider_root"]) + if navigation.get("provider_root") is not None + else evidence_value("unavailable"), + "entry": evidence_value("completed", navigation["entry"]) + if navigation.get("entry") is not None + else evidence_value("unavailable"), + }, + "counts": counts, + "orientation": dict(orientation), + "health": health, + "evidence": { + "deterministic": {"status": "completed", "findings": findings}, + "semantic": semantic_lane, + "unresolved": {"status": "completed", "candidates": list(unresolved)}, + }, + "doctor": dict(doctor), + "write_audit": { + "status": "completed", + "writes_attempted": evidence_value("completed", writes_attempted), + "writes_observed": evidence_value("completed", writes_observed), + }, + "git": { + "before": evidence_value("completed", git_before), + "after": evidence_value("completed", git_after), + }, + "unavailable_evidence": [], + } + receipt["unavailable_evidence"] = _collect_unavailable(receipt) + return validate_evidence_receipt(receipt) + + +def config_probe(path): + """Return bounded presence/digest evidence without parsing configuration code.""" + path = Path(path) + try: + size = path.stat().st_size + if size > 2 * 1024 * 1024: + raise ValueError("configuration exceeds capacity") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + except (OSError, ValueError): + return {"status": "unavailable", "sha256": None, "bytes": None} + return {"status": "completed", "sha256": f"sha256:{digest}", "bytes": size} + + +__all__ = ( + "EVIDENCE_RECEIPT_VERSION", + "EVIDENCE_STATES", + "MAX_RECEIPT_BYTES", + "build_evidence_receipt", + "canonical_receipt_bytes", + "config_probe", + "evidence_value", + "finding_receipt", + "health_receipt", + "observe_entry_orientation", + "validate_evidence_receipt", +) diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py b/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py index ec07ef2..96ba6dd 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.3" +SKILL_VERSION = "0.1.4" def canonical_request_bytes(value): diff --git a/adapters/plugin/skills/docs/scripts/evidence_receipt.py b/adapters/plugin/skills/docs/scripts/evidence_receipt.py new file mode 100644 index 0000000..ce0fbb9 --- /dev/null +++ b/adapters/plugin/skills/docs/scripts/evidence_receipt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Emit one sanitized Diataxis Docs evidence receipt to stdout.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from _docs_checker.evidence import ( + build_evidence_receipt, + canonical_receipt_bytes, + observe_entry_orientation, +) +from _docs_checker.health import health_summary +from _docs_checker.init_adoption import SKILL_VERSION +from check import check + + +_PARSER = argparse.ArgumentParser() +_PARSER.add_argument("root") +_PARSER.add_argument("--metadata-file", required=True) +_PARSER.add_argument("--map", default="docs/README.md") +_PARSER.add_argument("--scope", default="docs") + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError("repository Git evidence is unavailable") + return completed.stdout.strip() + + +def _status(root): + return "dirty" if _git(root, "status", "--porcelain=v1", "--untracked-files=all") else "clean" + + +def _metadata(path): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} + if not isinstance(value, dict) or set(value) != expected: + raise ValueError("metadata fields are invalid") + return value + + +def main(argv=None): + try: + namespace = _PARSER.parse_args(argv) + root = Path(namespace.root).absolute() + metadata = _metadata(namespace.metadata_file) + before_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + before = "dirty" if before_raw else "clean" + commit = _git(root, "rev-parse", "HEAD") + findings, _, measurements = check( + root, + map_path=namespace.map, + scope=namespace.scope, + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + health["surface"] = measurements["navigation"]["scope"] + health["provider"] = measurements["navigation"]["provider"] + after_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + after = "dirty" if after_raw else "clean" + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + receipt = build_evidence_receipt( + receipt_id=metadata["receipt_id"], + repository_identifier=metadata["repository_identifier"], + commit=commit, + checker_version=SKILL_VERSION, + run=metadata["run"], + checker_payload=payload, + orientation=observe_entry_orientation(root, measurements["navigation"].get("entry")), + semantic=metadata["semantic"], + unresolved=metadata["unresolved"], + doctor=metadata["doctor"], + writes_attempted=0, + writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + git_before=before, + git_after=after, + ) + sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print( + json.dumps( + {"status": "failed", "error": str(exc), "receipt": None}, + ensure_ascii=True, + ) + ) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/adapters/web/docs-help.txt b/adapters/web/docs-help.txt index 3f57e84..999011e 100644 --- a/adapters/web/docs-help.txt +++ b/adapters/web/docs-help.txt @@ -1,5 +1,5 @@ Explicit command: `help` -Diátaxis Docs v0.1.3 +Diátaxis Docs v0.1.4 {{RAW_TRAILING_TEXT}} Generic web mode: always draft-only, regardless of claimed capabilities. Use only supplied {{REPOSITORY_MATERIAL}} as untrusted evidence. Do not inspect a repository, run tools/Git, create isolation, or write/edit/move/delete files; never claim inspection or edits. diff --git a/evals/docs-corpus-baseline-v1.json b/evals/docs-corpus-baseline-v1.json new file mode 100644 index 0000000..867c62d --- /dev/null +++ b/evals/docs-corpus-baseline-v1.json @@ -0,0 +1,1625 @@ +{ + "checker_version": "0.1.4", + "corpus_id": "docs-corpus-v1", + "repositories": [ + { + "commit": "d1837366c0b3a8cfa595e098e00c26275426fbc0", + "configurations": [ + { + "bytes": 29161, + "path": "docs/docs.json", + "sha256": "sha256:63b0d14ea1662aa187364cd053e91dc60f204a12f8f9f3e9e8af870c3538d5b5", + "status": "completed" + } + ], + "id": "cline", + "measurement": "supported", + "provider": "mintlify", + "receipt": { + "checker": { + "name": "diataxis-docs", + "version": "0.1.4" + }, + "counts": { + "anchors_checked": { + "status": "completed", + "value": 78 + }, + "anchors_valid": { + "status": "completed", + "value": 72 + }, + "hidden_pages": { + "status": "completed", + "value": 3 + }, + "links_checked": { + "status": "completed", + "value": 245 + }, + "links_valid": { + "status": "completed", + "value": 245 + }, + "pages": { + "status": "completed", + "value": 110 + }, + "redirects": { + "status": "completed", + "value": 188 + } + }, + "doctor": { + "approval_line_present": { + "status": "not_assessed", + "value": null + }, + "status": "not_assessed", + "treatment_fingerprint": { + "status": "not_assessed", + "value": null + } + }, + "evidence": { + "deterministic": { + "findings": [ + { + "fingerprint": { + "status": "unavailable", + "value": null + }, + "kind": "missing-anchor", + "line": { + "status": "unavailable", + "value": null + }, + "path": { + "status": "completed", + "value": "docs/api/authentication.mdx" + }, + "target": { + "status": "completed", + "value": "/cli/cli-reference#cline-auth" + } + }, + { + "fingerprint": { + "status": "unavailable", + "value": null + }, + "kind": "missing-anchor", + "line": { + "status": "unavailable", + "value": null + }, + "path": { + "status": "completed", + "value": "docs/api/getting-started.mdx" + }, + "target": { + "status": "completed", + "value": "/api/models#free-models" + } + }, + { + "fingerprint": { + "status": "unavailable", + "value": null + }, + "kind": "missing-anchor", + "line": { + "status": "unavailable", + "value": null + }, + "path": { + "status": "completed", + "value": "docs/customization/cline-rules.mdx" + }, + "target": { + "status": "completed", + "value": "/getting-started/config#storage-locations" + } + }, + { + "fingerprint": { + "status": "unavailable", + "value": null + }, + "kind": "missing-anchor", + "line": { + "status": "unavailable", + "value": null + }, + "path": { + "status": "completed", + "value": "docs/customization/skills.mdx" + }, + "target": { + "status": "completed", + "value": "/getting-started/config#storage-locations" + } + }, + { + "fingerprint": { + "status": "unavailable", + "value": null + }, + "kind": "missing-anchor", + "line": { + "status": "unavailable", + "value": null + }, + "path": { + "status": "completed", + "value": "docs/enterprise-solutions/api-reference.mdx" + }, + "target": { + "status": "completed", + "value": "/api/overview#authentication" + } + }, + { + "fingerprint": { + "status": "unavailable", + "value": null + }, + "kind": "missing-anchor", + "line": { + "status": "unavailable", + "value": null + }, + "path": { + "status": "completed", + "value": "docs/tools-reference/all-cline-tools.mdx" + }, + "target": { + "status": "completed", + "value": "/sdk/tools#creating-custom-tools" + } + } + ], + "status": "completed" + }, + "semantic": { + "evaluator": { + "model": { + "status": "not_assessed", + "value": null + }, + "provider": { + "status": "not_assessed", + "value": null + }, + "version": { + "status": "not_assessed", + "value": null + } + }, + "findings": [], + "status": "not_assessed" + }, + "unresolved": { + "candidates": [], + "status": "completed" + } + }, + "git": { + "after": { + "status": "completed", + "value": "clean" + }, + "before": { + "status": "completed", + "value": "clean" + } + }, + "health": { + "available_weight": { + "status": "completed", + "value": 100 + }, + "categories": { + "anchors": { + "available": { + "status": "completed", + "value": 10 + }, + "earned": { + "status": "completed", + "value": 0 + }, + "raw": { + "checked": { + "status": "completed", + "value": 78 + }, + "valid": { + "status": "completed", + "value": 72 + } + } + }, + "entry": { + "available": { + "status": "completed", + "value": 20 + }, + "earned": { + "status": "completed", + "value": 5 + }, + "raw": { + "complete_single_document": { + "status": "completed", + "value": false + }, + "map_exists": { + "status": "completed", + "value": true + }, + "map_has_body": { + "status": "completed", + "value": true + }, + "map_has_h1": { + "status": "completed", + "value": false + }, + "map_has_h2": { + "status": "completed", + "value": true + }, + "useful_entry": { + "status": "completed", + "value": false + }, + "valid_navigation_routes": { + "status": "completed", + "value": 107 + } + } + }, + "links": { + "available": { + "status": "completed", + "value": 20 + }, + "earned": { + "status": "completed", + "value": 0 + }, + "raw": { + "checked": { + "status": "completed", + "value": 245 + }, + "valid": { + "status": "completed", + "value": 245 + } + } + }, + "path_safety": { + "available": { + "status": "completed", + "value": 15 + }, + "earned": { + "status": "completed", + "value": 15 + }, + "raw": { + "maintained": { + "status": "completed", + "value": 110 + }, + "safe": { + "status": "completed", + "value": 110 + } + } + }, + "reachability": { + "available": { + "status": "completed", + "value": 25 + }, + "earned": { + "status": "completed", + "value": 0 + }, + "raw": { + "maintained": { + "status": "completed", + "value": 110 + }, + "reachable": { + "status": "completed", + "value": 110 + } + } + }, + "titles": { + "available": { + "status": "completed", + "value": 10 + }, + "earned": { + "status": "completed", + "value": 9.45 + }, + "raw": { + "maintained": { + "status": "completed", + "value": 110 + }, + "usable_unique": { + "status": "completed", + "value": 104 + } + } + } + }, + "earned_weight": { + "status": "completed", + "value": 29.45 + }, + "percentage": { + "status": "completed", + "value": 29 + }, + "rubric_version": { + "status": "completed", + "value": 2 + }, + "score_gates": { + "map_has_h1": { + "status": "completed", + "value": false + }, + "useful_entry": { + "status": "completed", + "value": false + } + }, + "status": "completed" + }, + "orientation": { + "frontmatter_title": { + "status": "completed", + "value": true + }, + "literal_h1": { + "status": "completed", + "value": false + }, + "provider_rendered_title": { + "status": "unavailable", + "value": null + } + }, + "receipt_id": "docs-corpus-v1-cline", + "repository": { + "commit": { + "status": "completed", + "value": "d1837366c0b3a8cfa595e098e00c26275426fbc0" + }, + "identifier": "github.com/cline/cline" + }, + "run": { + "client": "local-corpus-harness", + "commands": [ + "corpus-check" + ], + "duration_seconds": { + "status": "not_assessed", + "value": null + }, + "effort": "not-applicable", + "id": "docs-corpus-v1-cline", + "model": "deterministic-harness", + "model_provider": "local", + "turns": { + "status": "completed", + "value": 1 + } + }, + "schema_version": 1, + "surface": { + "authority": { + "status": "completed", + "value": "docs/docs.json" + }, + "entry": { + "status": "completed", + "value": "docs/cline-overview.mdx" + }, + "provider": "mintlify", + "provider_root": { + "status": "completed", + "value": "docs" + } + }, + "unavailable_evidence": [ + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.deterministic.findings[0].fingerprint", + "evidence.deterministic.findings[0].line", + "evidence.deterministic.findings[1].fingerprint", + "evidence.deterministic.findings[1].line", + "evidence.deterministic.findings[2].fingerprint", + "evidence.deterministic.findings[2].line", + "evidence.deterministic.findings[3].fingerprint", + "evidence.deterministic.findings[3].line", + "evidence.deterministic.findings[4].fingerprint", + "evidence.deterministic.findings[4].line", + "evidence.deterministic.findings[5].fingerprint", + "evidence.deterministic.findings[5].line", + "evidence.semantic.evaluator.model", + "evidence.semantic.evaluator.provider", + "evidence.semantic.evaluator.version", + "orientation.provider_rendered_title", + "run.duration_seconds" + ], + "write_audit": { + "status": "completed", + "writes_attempted": { + "status": "completed", + "value": 0 + }, + "writes_observed": { + "status": "completed", + "value": 0 + } + } + } + }, + { + "commit": "c1d010a699a76738db63d22d33256b15bd7aea7a", + "configurations": [ + { + "bytes": 1211, + "path": "apps/docs/generator/config.ts", + "sha256": "sha256:6c1f5ec222852279ea32900b60e510da7ea3fece5775ae2c1a4deab114a5a58b", + "status": "completed" + }, + { + "bytes": 5019, + "path": "apps/docs/content/navigation.references.ts", + "sha256": "sha256:fb61ffe395f2bb6ed758006500744e163f3103fd6554c11045609c09050fd2dd", + "status": "completed" + } + ], + "id": "supabase", + "measurement": "unsupported", + "provider": "custom-mdx", + "receipt": { + "checker": { + "name": "diataxis-docs", + "version": "0.1.4" + }, + "counts": { + "anchors_checked": { + "status": "not_assessed", + "value": null + }, + "anchors_valid": { + "status": "not_assessed", + "value": null + }, + "hidden_pages": { + "status": "not_assessed", + "value": null + }, + "links_checked": { + "status": "not_assessed", + "value": null + }, + "links_valid": { + "status": "not_assessed", + "value": null + }, + "pages": { + "status": "not_assessed", + "value": null + }, + "redirects": { + "status": "not_assessed", + "value": null + } + }, + "doctor": { + "approval_line_present": { + "status": "not_assessed", + "value": null + }, + "status": "not_assessed", + "treatment_fingerprint": { + "status": "not_assessed", + "value": null + } + }, + "evidence": { + "deterministic": { + "findings": [], + "status": "completed" + }, + "semantic": { + "evaluator": { + "model": { + "status": "not_assessed", + "value": null + }, + "provider": { + "status": "not_assessed", + "value": null + }, + "version": { + "status": "not_assessed", + "value": null + } + }, + "findings": [], + "status": "not_assessed" + }, + "unresolved": { + "candidates": [ + { + "kind": "unsupported-custom-mdx-semantics", + "status": "not_assessed" + } + ], + "status": "completed" + } + }, + "git": { + "after": { + "status": "completed", + "value": "clean" + }, + "before": { + "status": "completed", + "value": "clean" + } + }, + "health": { + "available_weight": { + "status": "not_assessed", + "value": null + }, + "categories": {}, + "earned_weight": { + "status": "not_assessed", + "value": null + }, + "percentage": { + "status": "not_assessed", + "value": null + }, + "rubric_version": { + "status": "not_assessed", + "value": null + }, + "score_gates": { + "map_has_h1": { + "status": "not_assessed", + "value": null + }, + "useful_entry": { + "status": "not_assessed", + "value": null + } + }, + "status": "not_assessed" + }, + "orientation": { + "frontmatter_title": { + "status": "completed", + "value": true + }, + "literal_h1": { + "status": "completed", + "value": false + }, + "provider_rendered_title": { + "status": "unavailable", + "value": null + } + }, + "receipt_id": "docs-corpus-v1-supabase", + "repository": { + "commit": { + "status": "completed", + "value": "c1d010a699a76738db63d22d33256b15bd7aea7a" + }, + "identifier": "github.com/supabase/supabase" + }, + "run": { + "client": "local-corpus-harness", + "commands": [ + "corpus-check" + ], + "duration_seconds": { + "status": "not_assessed", + "value": null + }, + "effort": "not-applicable", + "id": "docs-corpus-v1-supabase", + "model": "deterministic-harness", + "model_provider": "local", + "turns": { + "status": "completed", + "value": 1 + } + }, + "schema_version": 1, + "surface": { + "authority": { + "status": "completed", + "value": "apps/docs/content/navigation.references.ts" + }, + "entry": { + "status": "completed", + "value": "apps/docs/content/guides/getting-started.mdx" + }, + "provider": "custom-mdx", + "provider_root": { + "status": "completed", + "value": "apps/docs/content" + } + }, + "unavailable_evidence": [ + "counts.anchors_checked", + "counts.anchors_valid", + "counts.hidden_pages", + "counts.links_checked", + "counts.links_valid", + "counts.pages", + "counts.redirects", + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.semantic.evaluator.model", + "evidence.semantic.evaluator.provider", + "evidence.semantic.evaluator.version", + "health.available_weight", + "health.earned_weight", + "health.percentage", + "health.rubric_version", + "health.score_gates.map_has_h1", + "health.score_gates.useful_entry", + "orientation.provider_rendered_title", + "run.duration_seconds" + ], + "write_audit": { + "status": "completed", + "writes_attempted": { + "status": "completed", + "value": 0 + }, + "writes_observed": { + "status": "completed", + "value": 0 + } + } + } + }, + { + "commit": "a0bc32214436d52a5ac9de9be1a515d872987366", + "configurations": [ + { + "bytes": 31230, + "path": "website/docusaurus.config.ts", + "sha256": "sha256:db6482643a938d96bc97331933fd732a61c3112b2f46fbea0812236b0805c1c6", + "status": "completed" + }, + { + "bytes": 4611, + "path": "website/sidebars.ts", + "sha256": "sha256:e785151eb62edd58b569b7c424887aec6412ef3a5d004fecc4dd2d4fb9660426", + "status": "completed" + }, + { + "bytes": 134, + "path": "website/versions.json", + "sha256": "sha256:241339c660b5cde4461aac1c693ece9182b7fcc2bc76ad5de7ab2df8c2ce562d", + "status": "completed" + } + ], + "id": "docusaurus", + "measurement": "unsupported", + "provider": "docusaurus", + "receipt": { + "checker": { + "name": "diataxis-docs", + "version": "0.1.4" + }, + "counts": { + "anchors_checked": { + "status": "not_assessed", + "value": null + }, + "anchors_valid": { + "status": "not_assessed", + "value": null + }, + "hidden_pages": { + "status": "not_assessed", + "value": null + }, + "links_checked": { + "status": "not_assessed", + "value": null + }, + "links_valid": { + "status": "not_assessed", + "value": null + }, + "pages": { + "status": "not_assessed", + "value": null + }, + "redirects": { + "status": "not_assessed", + "value": null + } + }, + "doctor": { + "approval_line_present": { + "status": "not_assessed", + "value": null + }, + "status": "not_assessed", + "treatment_fingerprint": { + "status": "not_assessed", + "value": null + } + }, + "evidence": { + "deterministic": { + "findings": [], + "status": "completed" + }, + "semantic": { + "evaluator": { + "model": { + "status": "not_assessed", + "value": null + }, + "provider": { + "status": "not_assessed", + "value": null + }, + "version": { + "status": "not_assessed", + "value": null + } + }, + "findings": [], + "status": "not_assessed" + }, + "unresolved": { + "candidates": [ + { + "kind": "unsupported-docusaurus-semantics", + "status": "not_assessed" + } + ], + "status": "completed" + } + }, + "git": { + "after": { + "status": "completed", + "value": "clean" + }, + "before": { + "status": "completed", + "value": "clean" + } + }, + "health": { + "available_weight": { + "status": "not_assessed", + "value": null + }, + "categories": {}, + "earned_weight": { + "status": "not_assessed", + "value": null + }, + "percentage": { + "status": "not_assessed", + "value": null + }, + "rubric_version": { + "status": "not_assessed", + "value": null + }, + "score_gates": { + "map_has_h1": { + "status": "not_assessed", + "value": null + }, + "useful_entry": { + "status": "not_assessed", + "value": null + } + }, + "status": "not_assessed" + }, + "orientation": { + "frontmatter_title": { + "status": "completed", + "value": false + }, + "literal_h1": { + "status": "completed", + "value": true + }, + "provider_rendered_title": { + "status": "unavailable", + "value": null + } + }, + "receipt_id": "docs-corpus-v1-docusaurus", + "repository": { + "commit": { + "status": "completed", + "value": "a0bc32214436d52a5ac9de9be1a515d872987366" + }, + "identifier": "github.com/facebook/docusaurus" + }, + "run": { + "client": "local-corpus-harness", + "commands": [ + "corpus-check" + ], + "duration_seconds": { + "status": "not_assessed", + "value": null + }, + "effort": "not-applicable", + "id": "docs-corpus-v1-docusaurus", + "model": "deterministic-harness", + "model_provider": "local", + "turns": { + "status": "completed", + "value": 1 + } + }, + "schema_version": 1, + "surface": { + "authority": { + "status": "completed", + "value": "website/docusaurus.config.ts" + }, + "entry": { + "status": "completed", + "value": "website/docs/introduction.mdx" + }, + "provider": "docusaurus", + "provider_root": { + "status": "completed", + "value": "website/docs" + } + }, + "unavailable_evidence": [ + "counts.anchors_checked", + "counts.anchors_valid", + "counts.hidden_pages", + "counts.links_checked", + "counts.links_valid", + "counts.pages", + "counts.redirects", + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.semantic.evaluator.model", + "evidence.semantic.evaluator.provider", + "evidence.semantic.evaluator.version", + "health.available_weight", + "health.earned_weight", + "health.percentage", + "health.rubric_version", + "health.score_gates.map_has_h1", + "health.score_gates.useful_entry", + "orientation.provider_rendered_title", + "run.duration_seconds" + ], + "write_audit": { + "status": "completed", + "writes_attempted": { + "status": "completed", + "value": 0 + }, + "writes_observed": { + "status": "completed", + "value": 0 + } + } + } + }, + { + "commit": "e16ff3a1199293ac9cdfa6132c08fdea162215f3", + "configurations": [ + { + "bytes": 17370, + "path": "docs/.vitepress/config.ts", + "sha256": "sha256:831762578cc7dd0604544300d43886e191c6340e23e365861898095ab79425a6", + "status": "completed" + } + ], + "id": "vite", + "measurement": "unsupported", + "provider": "vitepress", + "receipt": { + "checker": { + "name": "diataxis-docs", + "version": "0.1.4" + }, + "counts": { + "anchors_checked": { + "status": "not_assessed", + "value": null + }, + "anchors_valid": { + "status": "not_assessed", + "value": null + }, + "hidden_pages": { + "status": "not_assessed", + "value": null + }, + "links_checked": { + "status": "not_assessed", + "value": null + }, + "links_valid": { + "status": "not_assessed", + "value": null + }, + "pages": { + "status": "not_assessed", + "value": null + }, + "redirects": { + "status": "not_assessed", + "value": null + } + }, + "doctor": { + "approval_line_present": { + "status": "not_assessed", + "value": null + }, + "status": "not_assessed", + "treatment_fingerprint": { + "status": "not_assessed", + "value": null + } + }, + "evidence": { + "deterministic": { + "findings": [], + "status": "completed" + }, + "semantic": { + "evaluator": { + "model": { + "status": "not_assessed", + "value": null + }, + "provider": { + "status": "not_assessed", + "value": null + }, + "version": { + "status": "not_assessed", + "value": null + } + }, + "findings": [], + "status": "not_assessed" + }, + "unresolved": { + "candidates": [ + { + "kind": "unsupported-vitepress-semantics", + "status": "not_assessed" + } + ], + "status": "completed" + } + }, + "git": { + "after": { + "status": "completed", + "value": "clean" + }, + "before": { + "status": "completed", + "value": "clean" + } + }, + "health": { + "available_weight": { + "status": "not_assessed", + "value": null + }, + "categories": {}, + "earned_weight": { + "status": "not_assessed", + "value": null + }, + "percentage": { + "status": "not_assessed", + "value": null + }, + "rubric_version": { + "status": "not_assessed", + "value": null + }, + "score_gates": { + "map_has_h1": { + "status": "not_assessed", + "value": null + }, + "useful_entry": { + "status": "not_assessed", + "value": null + } + }, + "status": "not_assessed" + }, + "orientation": { + "frontmatter_title": { + "status": "completed", + "value": true + }, + "literal_h1": { + "status": "completed", + "value": false + }, + "provider_rendered_title": { + "status": "unavailable", + "value": null + } + }, + "receipt_id": "docs-corpus-v1-vite", + "repository": { + "commit": { + "status": "completed", + "value": "e16ff3a1199293ac9cdfa6132c08fdea162215f3" + }, + "identifier": "github.com/vitejs/vite" + }, + "run": { + "client": "local-corpus-harness", + "commands": [ + "corpus-check" + ], + "duration_seconds": { + "status": "not_assessed", + "value": null + }, + "effort": "not-applicable", + "id": "docs-corpus-v1-vite", + "model": "deterministic-harness", + "model_provider": "local", + "turns": { + "status": "completed", + "value": 1 + } + }, + "schema_version": 1, + "surface": { + "authority": { + "status": "completed", + "value": "docs/.vitepress/config.ts" + }, + "entry": { + "status": "completed", + "value": "docs/index.md" + }, + "provider": "vitepress", + "provider_root": { + "status": "completed", + "value": "docs" + } + }, + "unavailable_evidence": [ + "counts.anchors_checked", + "counts.anchors_valid", + "counts.hidden_pages", + "counts.links_checked", + "counts.links_valid", + "counts.pages", + "counts.redirects", + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.semantic.evaluator.model", + "evidence.semantic.evaluator.provider", + "evidence.semantic.evaluator.version", + "health.available_weight", + "health.earned_weight", + "health.percentage", + "health.rubric_version", + "health.score_gates.map_has_h1", + "health.score_gates.useful_entry", + "orientation.provider_rendered_title", + "run.duration_seconds" + ], + "write_audit": { + "status": "completed", + "writes_attempted": { + "status": "completed", + "value": 0 + }, + "writes_observed": { + "status": "completed", + "value": 0 + } + } + } + }, + { + "commit": "bb9eba0bb4e04c08d8a09f1096ede335e54e5503", + "configurations": [ + { + "bytes": 11407, + "path": "mkdocs.yml", + "sha256": "sha256:e39d302d07d5e355ba2734de630a01075b9292ff2a07016e5eaa15161d784d27", + "status": "completed" + } + ], + "id": "uv", + "measurement": "unsupported", + "provider": "mkdocs", + "receipt": { + "checker": { + "name": "diataxis-docs", + "version": "0.1.4" + }, + "counts": { + "anchors_checked": { + "status": "not_assessed", + "value": null + }, + "anchors_valid": { + "status": "not_assessed", + "value": null + }, + "hidden_pages": { + "status": "not_assessed", + "value": null + }, + "links_checked": { + "status": "not_assessed", + "value": null + }, + "links_valid": { + "status": "not_assessed", + "value": null + }, + "pages": { + "status": "not_assessed", + "value": null + }, + "redirects": { + "status": "not_assessed", + "value": null + } + }, + "doctor": { + "approval_line_present": { + "status": "not_assessed", + "value": null + }, + "status": "not_assessed", + "treatment_fingerprint": { + "status": "not_assessed", + "value": null + } + }, + "evidence": { + "deterministic": { + "findings": [], + "status": "completed" + }, + "semantic": { + "evaluator": { + "model": { + "status": "not_assessed", + "value": null + }, + "provider": { + "status": "not_assessed", + "value": null + }, + "version": { + "status": "not_assessed", + "value": null + } + }, + "findings": [], + "status": "not_assessed" + }, + "unresolved": { + "candidates": [ + { + "kind": "unsupported-mkdocs-semantics", + "status": "not_assessed" + } + ], + "status": "completed" + } + }, + "git": { + "after": { + "status": "completed", + "value": "clean" + }, + "before": { + "status": "completed", + "value": "clean" + } + }, + "health": { + "available_weight": { + "status": "not_assessed", + "value": null + }, + "categories": {}, + "earned_weight": { + "status": "not_assessed", + "value": null + }, + "percentage": { + "status": "not_assessed", + "value": null + }, + "rubric_version": { + "status": "not_assessed", + "value": null + }, + "score_gates": { + "map_has_h1": { + "status": "not_assessed", + "value": null + }, + "useful_entry": { + "status": "not_assessed", + "value": null + } + }, + "status": "not_assessed" + }, + "orientation": { + "frontmatter_title": { + "status": "completed", + "value": false + }, + "literal_h1": { + "status": "completed", + "value": true + }, + "provider_rendered_title": { + "status": "unavailable", + "value": null + } + }, + "receipt_id": "docs-corpus-v1-uv", + "repository": { + "commit": { + "status": "completed", + "value": "bb9eba0bb4e04c08d8a09f1096ede335e54e5503" + }, + "identifier": "github.com/astral-sh/uv" + }, + "run": { + "client": "local-corpus-harness", + "commands": [ + "corpus-check" + ], + "duration_seconds": { + "status": "not_assessed", + "value": null + }, + "effort": "not-applicable", + "id": "docs-corpus-v1-uv", + "model": "deterministic-harness", + "model_provider": "local", + "turns": { + "status": "completed", + "value": 1 + } + }, + "schema_version": 1, + "surface": { + "authority": { + "status": "completed", + "value": "mkdocs.yml" + }, + "entry": { + "status": "completed", + "value": "docs/index.md" + }, + "provider": "mkdocs", + "provider_root": { + "status": "completed", + "value": "docs" + } + }, + "unavailable_evidence": [ + "counts.anchors_checked", + "counts.anchors_valid", + "counts.hidden_pages", + "counts.links_checked", + "counts.links_valid", + "counts.pages", + "counts.redirects", + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.semantic.evaluator.model", + "evidence.semantic.evaluator.provider", + "evidence.semantic.evaluator.version", + "health.available_weight", + "health.earned_weight", + "health.percentage", + "health.rubric_version", + "health.score_gates.map_has_h1", + "health.score_gates.useful_entry", + "orientation.provider_rendered_title", + "run.duration_seconds" + ], + "write_audit": { + "status": "completed", + "writes_attempted": { + "status": "completed", + "value": 0 + }, + "writes_observed": { + "status": "completed", + "value": 0 + } + } + } + }, + { + "commit": "5e1d1bde0ca03efe09608d59c573d6ec87052c24", + "configurations": [ + { + "bytes": 18452, + "path": "hugo.toml", + "sha256": "sha256:83a0eef8dc9d68fb286e694a6b85cd80ed4d170d69eaba044d5a02028ce0b902", + "status": "completed" + } + ], + "id": "kubernetes-website", + "measurement": "unsupported", + "provider": "hugo", + "receipt": { + "checker": { + "name": "diataxis-docs", + "version": "0.1.4" + }, + "counts": { + "anchors_checked": { + "status": "not_assessed", + "value": null + }, + "anchors_valid": { + "status": "not_assessed", + "value": null + }, + "hidden_pages": { + "status": "not_assessed", + "value": null + }, + "links_checked": { + "status": "not_assessed", + "value": null + }, + "links_valid": { + "status": "not_assessed", + "value": null + }, + "pages": { + "status": "not_assessed", + "value": null + }, + "redirects": { + "status": "not_assessed", + "value": null + } + }, + "doctor": { + "approval_line_present": { + "status": "not_assessed", + "value": null + }, + "status": "not_assessed", + "treatment_fingerprint": { + "status": "not_assessed", + "value": null + } + }, + "evidence": { + "deterministic": { + "findings": [], + "status": "completed" + }, + "semantic": { + "evaluator": { + "model": { + "status": "not_assessed", + "value": null + }, + "provider": { + "status": "not_assessed", + "value": null + }, + "version": { + "status": "not_assessed", + "value": null + } + }, + "findings": [], + "status": "not_assessed" + }, + "unresolved": { + "candidates": [ + { + "kind": "unsupported-hugo-semantics", + "status": "not_assessed" + } + ], + "status": "completed" + } + }, + "git": { + "after": { + "status": "completed", + "value": "clean" + }, + "before": { + "status": "completed", + "value": "clean" + } + }, + "health": { + "available_weight": { + "status": "not_assessed", + "value": null + }, + "categories": {}, + "earned_weight": { + "status": "not_assessed", + "value": null + }, + "percentage": { + "status": "not_assessed", + "value": null + }, + "rubric_version": { + "status": "not_assessed", + "value": null + }, + "score_gates": { + "map_has_h1": { + "status": "not_assessed", + "value": null + }, + "useful_entry": { + "status": "not_assessed", + "value": null + } + }, + "status": "not_assessed" + }, + "orientation": { + "frontmatter_title": { + "status": "completed", + "value": true + }, + "literal_h1": { + "status": "completed", + "value": false + }, + "provider_rendered_title": { + "status": "unavailable", + "value": null + } + }, + "receipt_id": "docs-corpus-v1-kubernetes-website", + "repository": { + "commit": { + "status": "completed", + "value": "5e1d1bde0ca03efe09608d59c573d6ec87052c24" + }, + "identifier": "github.com/kubernetes/website" + }, + "run": { + "client": "local-corpus-harness", + "commands": [ + "corpus-check" + ], + "duration_seconds": { + "status": "not_assessed", + "value": null + }, + "effort": "not-applicable", + "id": "docs-corpus-v1-kubernetes-website", + "model": "deterministic-harness", + "model_provider": "local", + "turns": { + "status": "completed", + "value": 1 + } + }, + "schema_version": 1, + "surface": { + "authority": { + "status": "completed", + "value": "hugo.toml" + }, + "entry": { + "status": "completed", + "value": "content/en/docs/home/_index.md" + }, + "provider": "hugo", + "provider_root": { + "status": "completed", + "value": "content" + } + }, + "unavailable_evidence": [ + "counts.anchors_checked", + "counts.anchors_valid", + "counts.hidden_pages", + "counts.links_checked", + "counts.links_valid", + "counts.pages", + "counts.redirects", + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.semantic.evaluator.model", + "evidence.semantic.evaluator.provider", + "evidence.semantic.evaluator.version", + "health.available_weight", + "health.earned_weight", + "health.percentage", + "health.rubric_version", + "health.score_gates.map_has_h1", + "health.score_gates.useful_entry", + "orientation.provider_rendered_title", + "run.duration_seconds" + ], + "write_audit": { + "status": "completed", + "writes_attempted": { + "status": "completed", + "value": 0 + }, + "writes_observed": { + "status": "completed", + "value": 0 + } + } + } + } + ], + "rubric": { + "changed": false, + "version": 2, + "weights": { + "anchors": 10, + "entry": 20, + "links": 20, + "path_safety": 15, + "reachability": 25, + "titles": 10 + } + }, + "schema_version": 1 +} diff --git a/evals/docs-corpus-v1.json b/evals/docs-corpus-v1.json new file mode 100644 index 0000000..c8e6ac3 --- /dev/null +++ b/evals/docs-corpus-v1.json @@ -0,0 +1,78 @@ +{ + "schema_version": 1, + "corpus_id": "docs-corpus-v1", + "repositories": [ + { + "id": "cline", + "repository_url": "https://github.com/cline/cline.git", + "commit": "d1837366c0b3a8cfa595e098e00c26275426fbc0", + "provider": "mintlify", + "measurement": "supported", + "scope": "docs", + "entry": "docs/cline-overview.mdx", + "authority_probes": ["docs/docs.json"], + "config_probes": ["docs/docs.json"], + "sparse_paths": ["README.md", "docs"] + }, + { + "id": "supabase", + "repository_url": "https://github.com/supabase/supabase.git", + "commit": "c1d010a699a76738db63d22d33256b15bd7aea7a", + "provider": "custom-mdx", + "measurement": "unsupported", + "scope": "apps/docs/content", + "entry": "apps/docs/content/guides/getting-started.mdx", + "authority_probes": ["apps/docs/content/navigation.references.ts"], + "config_probes": ["apps/docs/generator/config.ts", "apps/docs/content/navigation.references.ts"], + "sparse_paths": ["apps/docs/README.md", "apps/docs/content", "apps/docs/generator/config.ts"] + }, + { + "id": "docusaurus", + "repository_url": "https://github.com/facebook/docusaurus.git", + "commit": "a0bc32214436d52a5ac9de9be1a515d872987366", + "provider": "docusaurus", + "measurement": "unsupported", + "scope": "website/docs", + "entry": "website/docs/introduction.mdx", + "authority_probes": ["website/docusaurus.config.ts", "website/sidebars.ts"], + "config_probes": ["website/docusaurus.config.ts", "website/sidebars.ts", "website/versions.json"], + "sparse_paths": ["website/docs", "website/docusaurus.config.ts", "website/sidebars.ts", "website/versioned_docs", "website/versioned_sidebars", "website/versions.json"] + }, + { + "id": "vite", + "repository_url": "https://github.com/vitejs/vite.git", + "commit": "e16ff3a1199293ac9cdfa6132c08fdea162215f3", + "provider": "vitepress", + "measurement": "unsupported", + "scope": "docs", + "entry": "docs/index.md", + "authority_probes": ["docs/.vitepress/config.ts"], + "config_probes": ["docs/.vitepress/config.ts"], + "sparse_paths": ["docs"] + }, + { + "id": "uv", + "repository_url": "https://github.com/astral-sh/uv.git", + "commit": "bb9eba0bb4e04c08d8a09f1096ede335e54e5503", + "provider": "mkdocs", + "measurement": "unsupported", + "scope": "docs", + "entry": "docs/index.md", + "authority_probes": ["mkdocs.yml"], + "config_probes": ["mkdocs.yml"], + "sparse_paths": ["docs", "mkdocs.yml"] + }, + { + "id": "kubernetes-website", + "repository_url": "https://github.com/kubernetes/website.git", + "commit": "5e1d1bde0ca03efe09608d59c573d6ec87052c24", + "provider": "hugo", + "measurement": "unsupported", + "scope": "content", + "entry": "content/en/docs/home/_index.md", + "authority_probes": ["hugo.toml"], + "config_probes": ["hugo.toml"], + "sparse_paths": ["config", "content", "data", "hugo.toml", "layouts"] + } + ] +} diff --git a/evals/dogfood/cline-0.1.3.json b/evals/dogfood/cline-0.1.3.json new file mode 100644 index 0000000..479da50 --- /dev/null +++ b/evals/dogfood/cline-0.1.3.json @@ -0,0 +1,180 @@ +{ + "schema_version": 1, + "receipt_id": "dogfood-cline-0.1.3", + "repository": { + "identifier": "github.com/cline/cline", + "commit": {"status": "unavailable", "value": null} + }, + "checker": {"name": "diataxis-docs", "version": "0.1.3"}, + "run": { + "id": "019f71fd-aa0d-7093-821c-b5a2b98975e7", + "client": "codex-desktop-vscode", + "model_provider": "openai", + "model": "gpt-5.6-luna", + "effort": "max", + "turns": {"status": "completed", "value": 1}, + "duration_seconds": {"status": "completed", "value": 508.747}, + "commands": ["docs-map", "docs-check", "docs-audit", "docs-doctor"] + }, + "surface": { + "provider": "mintlify", + "authority": {"status": "completed", "value": "docs/docs.json"}, + "provider_root": {"status": "completed", "value": "docs"}, + "entry": {"status": "completed", "value": "docs/cline-overview.mdx"} + }, + "counts": { + "pages": {"status": "completed", "value": 107}, + "hidden_pages": {"status": "completed", "value": 3}, + "redirects": {"status": "completed", "value": 188}, + "links_checked": {"status": "completed", "value": 245}, + "links_valid": {"status": "completed", "value": 245}, + "anchors_checked": {"status": "completed", "value": 78}, + "anchors_valid": {"status": "completed", "value": 72} + }, + "orientation": { + "literal_h1": {"status": "completed", "value": false}, + "frontmatter_title": {"status": "completed", "value": true}, + "provider_rendered_title": {"status": "unavailable", "value": null} + }, + "health": { + "status": "completed", + "rubric_version": {"status": "completed", "value": 2}, + "percentage": {"status": "completed", "value": 29}, + "earned_weight": {"status": "unavailable", "value": null}, + "available_weight": {"status": "completed", "value": 100}, + "categories": { + "entry": { + "raw": { + "map_exists": {"status": "unavailable", "value": null}, + "map_has_h1": {"status": "completed", "value": false}, + "map_has_body": {"status": "unavailable", "value": null}, + "map_has_h2": {"status": "unavailable", "value": null}, + "valid_navigation_routes": {"status": "unavailable", "value": null}, + "complete_single_document": {"status": "unavailable", "value": null}, + "useful_entry": {"status": "completed", "value": false} + }, + "earned": {"status": "unavailable", "value": null}, + "available": {"status": "completed", "value": 20} + }, + "path_safety": { + "raw": { + "safe": {"status": "unavailable", "value": null}, + "maintained": {"status": "unavailable", "value": null} + }, + "earned": {"status": "unavailable", "value": null}, + "available": {"status": "completed", "value": 15} + }, + "links": { + "raw": { + "valid": {"status": "completed", "value": 245}, + "checked": {"status": "completed", "value": 245} + }, + "earned": {"status": "unavailable", "value": null}, + "available": {"status": "completed", "value": 20} + }, + "anchors": { + "raw": { + "valid": {"status": "completed", "value": 72}, + "checked": {"status": "completed", "value": 78} + }, + "earned": {"status": "unavailable", "value": null}, + "available": {"status": "completed", "value": 10} + }, + "reachability": { + "raw": { + "reachable": {"status": "unavailable", "value": null}, + "maintained": {"status": "completed", "value": 107} + }, + "earned": {"status": "unavailable", "value": null}, + "available": {"status": "completed", "value": 25} + }, + "titles": { + "raw": { + "usable_unique": {"status": "unavailable", "value": null}, + "maintained": {"status": "completed", "value": 107} + }, + "earned": {"status": "unavailable", "value": null}, + "available": {"status": "completed", "value": 10} + } + }, + "score_gates": { + "map_has_h1": {"status": "completed", "value": false}, + "useful_entry": {"status": "completed", "value": false} + } + }, + "evidence": { + "deterministic": { + "status": "completed", + "findings": [ + {"kind": "missing-anchor", "path": {"status": "completed", "value": "docs/api/authentication.mdx"}, "line": {"status": "completed", "value": 72}, "target": {"status": "completed", "value": "/cli/cli-reference#cline-auth"}, "fingerprint": {"status": "unavailable", "value": null}}, + {"kind": "missing-anchor", "path": {"status": "completed", "value": "docs/api/getting-started.mdx"}, "line": {"status": "completed", "value": 97}, "target": {"status": "completed", "value": "/api/models#free-models"}, "fingerprint": {"status": "unavailable", "value": null}}, + {"kind": "missing-anchor", "path": {"status": "completed", "value": "docs/customization/cline-rules.mdx"}, "line": {"status": "completed", "value": 54}, "target": {"status": "completed", "value": "/getting-started/config#storage-locations"}, "fingerprint": {"status": "unavailable", "value": null}}, + {"kind": "missing-anchor", "path": {"status": "completed", "value": "docs/customization/skills.mdx"}, "line": {"status": "completed", "value": 151}, "target": {"status": "completed", "value": "/getting-started/config#storage-locations"}, "fingerprint": {"status": "unavailable", "value": null}}, + {"kind": "missing-anchor", "path": {"status": "completed", "value": "docs/enterprise-solutions/api-reference.mdx"}, "line": {"status": "completed", "value": 23}, "target": {"status": "completed", "value": "/api/overview#authentication"}, "fingerprint": {"status": "unavailable", "value": null}}, + {"kind": "missing-anchor", "path": {"status": "completed", "value": "docs/tools-reference/all-cline-tools.mdx"}, "line": {"status": "completed", "value": 67}, "target": {"status": "completed", "value": "/sdk/tools#creating-custom-tools"}, "fingerprint": {"status": "unavailable", "value": null}} + ] + }, + "semantic": { + "status": "completed", + "evaluator": { + "provider": {"status": "completed", "value": "openai"}, + "model": {"status": "completed", "value": "gpt-5.6-luna"}, + "version": {"status": "unavailable", "value": null} + }, + "findings": [] + }, + "unresolved": { + "status": "completed", + "candidates": [ + {"kind": "canonical-diataxis-map", "status": "unavailable"}, + {"kind": "doctor-treatment-fingerprint", "status": "unavailable"}, + {"kind": "approval-line-evidence", "status": "unavailable"}, + {"kind": "category-health-receipt", "status": "unavailable"} + ] + } + }, + "doctor": { + "status": "completed", + "treatment_fingerprint": {"status": "unavailable", "value": null}, + "approval_line_present": {"status": "unavailable", "value": null} + }, + "write_audit": { + "status": "completed", + "writes_attempted": {"status": "completed", "value": 0}, + "writes_observed": {"status": "completed", "value": 0} + }, + "git": { + "before": {"status": "unavailable", "value": null}, + "after": {"status": "completed", "value": "clean"} + }, + "unavailable_evidence": [ + "doctor.approval_line_present", + "doctor.treatment_fingerprint", + "evidence.deterministic.findings[0].fingerprint", + "evidence.deterministic.findings[1].fingerprint", + "evidence.deterministic.findings[2].fingerprint", + "evidence.deterministic.findings[3].fingerprint", + "evidence.deterministic.findings[4].fingerprint", + "evidence.deterministic.findings[5].fingerprint", + "evidence.semantic.evaluator.version", + "git.before", + "health.categories.anchors.earned", + "health.categories.entry.earned", + "health.categories.entry.raw.complete_single_document", + "health.categories.entry.raw.map_exists", + "health.categories.entry.raw.map_has_body", + "health.categories.entry.raw.map_has_h2", + "health.categories.entry.raw.valid_navigation_routes", + "health.categories.links.earned", + "health.categories.path_safety.earned", + "health.categories.path_safety.raw.maintained", + "health.categories.path_safety.raw.safe", + "health.categories.reachability.earned", + "health.categories.reachability.raw.reachable", + "health.categories.titles.earned", + "health.categories.titles.raw.usable_unique", + "health.earned_weight", + "orientation.provider_rendered_title", + "repository.commit" + ] +} diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md index 628596b..7a6cf74 100644 --- a/skills/docs/SKILL.md +++ b/skills/docs/SKILL.md @@ -3,7 +3,7 @@ name: docs description: Use when a user explicitly invokes repository documentation help for bounded Diátaxis writing, context recall, mapping, auditing, checking, migration, cleanup, Doctor guidance, or evidence-backed updates. metadata: author: Statusnone - version: "0.1.3" + version: "0.1.4" --- # Diátaxis Docs diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py new file mode 100644 index 0000000..8c86091 --- /dev/null +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -0,0 +1,674 @@ +"""Versioned, sanitized product-evidence receipts. + +This module stores facts, explicit absence, and lane provenance. It never +accepts transcript-shaped data or calculates the deterministic health score. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +from collections.abc import Mapping, Sequence +from pathlib import Path + +from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars +from .paths import normalize_repo_relative, safe_path + + +EVIDENCE_RECEIPT_VERSION = 1 +EVIDENCE_STATES = frozenset({"completed", "not_assessed", "unavailable", "failed"}) +MAX_RECEIPT_BYTES = 512 * 1024 +MAX_TEXT_BYTES = 512 +MAX_FINDINGS = 10_000 + +_SHA = re.compile(r"^[0-9a-f]{40}$") +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$") +_FORBIDDEN_KEY = re.compile( + r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", + re.IGNORECASE, +) +_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_CATEGORY_RAW_FIELDS = { + "entry": ( + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "valid_navigation_routes", + "complete_single_document", + "useful_entry", + ), + "path_safety": ("safe", "maintained"), + "links": ("valid", "checked"), + "anchors": ("valid", "checked"), + "reachability": ("reachable", "maintained"), + "titles": ("usable_unique", "maintained"), +} +_RECEIPT_FIELDS = frozenset( + { + "schema_version", + "receipt_id", + "repository", + "checker", + "run", + "surface", + "counts", + "orientation", + "health", + "evidence", + "doctor", + "write_audit", + "git", + "unavailable_evidence", + } +) + + +def evidence_value(status, value=None): + """Create one explicit evidence value without treating absence as zero.""" + if status not in EVIDENCE_STATES: + raise ValueError("evidence status is invalid") + if status == "completed" and value is None: + raise ValueError("completed evidence requires a value") + if status != "completed" and value is not None: + raise ValueError("incomplete evidence value must be null") + return {"status": status, "value": value} + + +def _mapping(value, name): + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _sequence(value, name): + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise ValueError(f"{name} must be an array") + return value + + +def _exact_keys(value, expected, name): + value = _mapping(value, name) + if set(value) != set(expected): + raise ValueError(f"{name} fields are invalid") + return value + + +def _bounded_text(value, name, *, pattern=None, allow_empty=False): + if not isinstance(value, str) or (not value and not allow_empty): + raise ValueError(f"{name} must be text") + if len(value.encode("utf-8", "strict")) > MAX_TEXT_BYTES: + raise ValueError(f"{name} exceeds capacity") + if any(ord(char) < 32 for char in value): + raise ValueError(f"{name} contains control characters") + if _WINDOWS_ABSOLUTE.match(value) or value.startswith(("/home/", "/Users/")): + raise ValueError(f"{name} exposes an absolute path") + if pattern is not None and pattern.fullmatch(value) is None: + raise ValueError(f"{name} is invalid") + return value + + +def _safe_identifier(value, name): + return _bounded_text(value, name, pattern=_SAFE_ID) + + +def _nonnegative_number(value, name): + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be numeric") + if not math.isfinite(value) or value < 0: + raise ValueError(f"{name} must be finite and nonnegative") + return value + + +def _evidence(value, name, *, validator=None): + value = _exact_keys(value, {"status", "value"}, name) + status = value["status"] + if status not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + current = value["value"] + if status == "completed": + if current is None: + raise ValueError(f"{name}.value is required") + if validator is not None: + validator(current, f"{name}.value") + elif current is not None: + raise ValueError(f"{name}.value must be null") + return {"status": status, "value": current} + + +def _integer(value, name): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{name} must be a nonnegative integer") + return value + + +def _boolean(value, name): + if not isinstance(value, bool): + raise ValueError(f"{name} must be boolean") + return value + + +def _sha(value, name): + if not isinstance(value, str) or _SHA.fullmatch(value) is None: + raise ValueError(f"{name} must be a lowercase full commit SHA") + return value + + +def _digest(value, name): + if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: + raise ValueError(f"{name} must be a SHA-256 digest") + return value + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return value + + +def _route(value, name): + value = _bounded_text(value, name) + if value.startswith(("http://", "https://", "file:")): + raise ValueError(f"{name} must be a local route") + return value + + +def _reject_forbidden_keys(value, name="receipt"): + if isinstance(value, Mapping): + for key, child in value.items(): + if not isinstance(key, str) or _FORBIDDEN_KEY.search(key): + raise ValueError(f"{name} contains a forbidden field") + _reject_forbidden_keys(child, f"{name}.{key}") + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + _reject_forbidden_keys(child, f"{name}[{index}]") + + +def _validate_run(value): + value = _exact_keys( + value, + {"id", "client", "model_provider", "model", "effort", "turns", "duration_seconds", "commands"}, + "run", + ) + for field in ("id", "client", "model_provider", "model", "effort"): + _safe_identifier(value[field], f"run.{field}") + _evidence(value["turns"], "run.turns", validator=_integer) + _evidence(value["duration_seconds"], "run.duration_seconds", validator=_nonnegative_number) + commands = _sequence(value["commands"], "run.commands") + if not commands or len(commands) > 32: + raise ValueError("run.commands count is invalid") + for index, command in enumerate(commands): + _safe_identifier(command, f"run.commands[{index}]") + + +def _validate_finding(value, name): + value = _exact_keys(value, {"kind", "path", "line", "target", "fingerprint"}, name) + _safe_identifier(value["kind"], f"{name}.kind") + _evidence(value["path"], f"{name}.path", validator=_relative) + _evidence(value["line"], f"{name}.line", validator=_integer) + _evidence(value["target"], f"{name}.target", validator=_route) + _evidence(value["fingerprint"], f"{name}.fingerprint", validator=_digest) + + +def _validate_findings(value, name): + findings = _sequence(value, name) + if len(findings) > MAX_FINDINGS: + raise ValueError(f"{name} exceeds capacity") + for index, finding in enumerate(findings): + _validate_finding(finding, f"{name}[{index}]") + + +def _validate_lane(value, name, *, semantic=False): + expected = {"status", "findings"} + if semantic: + expected.add("evaluator") + value = _exact_keys(value, expected, name) + if value["status"] not in EVIDENCE_STATES: + raise ValueError(f"{name}.status is invalid") + _validate_findings(value["findings"], f"{name}.findings") + if value["status"] != "completed" and value["findings"]: + raise ValueError(f"{name} cannot contain findings when incomplete") + if semantic: + evaluator = _exact_keys( + value["evaluator"], {"provider", "model", "version"}, f"{name}.evaluator" + ) + for field in evaluator: + _evidence( + evaluator[field], + f"{name}.evaluator.{field}", + validator=_safe_identifier, + ) + + +def _validate_unresolved(value): + value = _exact_keys(value, {"status", "candidates"}, "evidence.unresolved") + if value["status"] not in EVIDENCE_STATES: + raise ValueError("evidence.unresolved.status is invalid") + candidates = _sequence(value["candidates"], "evidence.unresolved.candidates") + if len(candidates) > 1_000: + raise ValueError("evidence.unresolved.candidates exceeds capacity") + for index, candidate in enumerate(candidates): + candidate = _exact_keys( + candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" + ) + _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") + if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: + raise ValueError("unresolved candidate status is invalid") + + +def _validate_categories(value): + value = _mapping(value, "health.categories") + if set(value) - set(_CATEGORY_RAW_FIELDS): + raise ValueError("health.categories contains an unknown category") + for category, fields in _CATEGORY_RAW_FIELDS.items(): + if category not in value: + continue + row = _exact_keys( + value[category], {"raw", "earned", "available"}, f"health.categories.{category}" + ) + raw = _exact_keys(row["raw"], fields, f"health.categories.{category}.raw") + for field, evidence in raw.items(): + validator = _boolean if field in { + "map_exists", + "map_has_h1", + "map_has_body", + "map_has_h2", + "complete_single_document", + "useful_entry", + } else _integer + _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) + _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) + _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + + +def _collect_unavailable(value, path=""): + results = [] + if isinstance(value, Mapping): + if set(value) == {"status", "value"} and value["status"] != "completed": + results.append(path) + else: + for key, child in value.items(): + if key == "unavailable_evidence": + continue + child_path = f"{path}.{key}" if path else key + results.extend(_collect_unavailable(child, child_path)) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for index, child in enumerate(value): + results.extend(_collect_unavailable(child, f"{path}[{index}]")) + return sorted(results) + + +def validate_evidence_receipt(value): + """Validate and return one exact schema-v1 sanitized receipt.""" + value = _exact_keys(value, _RECEIPT_FIELDS, "receipt") + _reject_forbidden_keys(value) + if value["schema_version"] != EVIDENCE_RECEIPT_VERSION: + raise ValueError("receipt schema_version is invalid") + _safe_identifier(value["receipt_id"], "receipt_id") + + repository = _exact_keys(value["repository"], {"identifier", "commit"}, "repository") + _safe_identifier(repository["identifier"], "repository.identifier") + _evidence(repository["commit"], "repository.commit", validator=_sha) + + checker = _exact_keys(value["checker"], {"name", "version"}, "checker") + _safe_identifier(checker["name"], "checker.name") + _safe_identifier(checker["version"], "checker.version") + _validate_run(value["run"]) + + surface = _exact_keys( + value["surface"], {"provider", "authority", "provider_root", "entry"}, "surface" + ) + _safe_identifier(surface["provider"], "surface.provider") + for field in ("authority", "provider_root", "entry"): + _evidence(surface[field], f"surface.{field}", validator=_relative) + + counts = _exact_keys( + value["counts"], + {"pages", "hidden_pages", "redirects", "links_checked", "links_valid", "anchors_checked", "anchors_valid"}, + "counts", + ) + for field, current in counts.items(): + _evidence(current, f"counts.{field}", validator=_integer) + + orientation = _exact_keys( + value["orientation"], + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + + health = _exact_keys( + value["health"], + {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, + "health", + ) + if health["status"] not in EVIDENCE_STATES: + raise ValueError("health.status is invalid") + _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) + for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health[field], f"health.{field}", validator=_nonnegative_number) + _validate_categories(health["categories"]) + gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") + for field in gates: + _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) + + evidence = _exact_keys( + value["evidence"], {"deterministic", "semantic", "unresolved"}, "evidence" + ) + _validate_lane(evidence["deterministic"], "evidence.deterministic") + _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) + _validate_unresolved(evidence["unresolved"]) + + doctor = _exact_keys( + value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + if doctor["status"] not in EVIDENCE_STATES: + raise ValueError("doctor.status is invalid") + _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + + write_audit = _exact_keys( + value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" + ) + if write_audit["status"] not in EVIDENCE_STATES: + raise ValueError("write_audit.status is invalid") + _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) + _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + + git = _exact_keys(value["git"], {"before", "after"}, "git") + for field in git: + _evidence( + git[field], + f"git.{field}", + validator=lambda current, name: _bounded_text( + current, name, pattern=re.compile(r"^(?:clean|dirty)$") + ), + ) + + unavailable = _sequence(value["unavailable_evidence"], "unavailable_evidence") + for index, field in enumerate(unavailable): + _bounded_text(field, f"unavailable_evidence[{index}]") + expected_unavailable = _collect_unavailable(value) + if list(unavailable) != expected_unavailable: + raise ValueError("unavailable_evidence does not match explicit evidence states") + + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + if len(encoded) > MAX_RECEIPT_BYTES: + raise ValueError("receipt exceeds capacity") + return value + + +def canonical_receipt_bytes(value): + validated = validate_evidence_receipt(value) + return ( + json.dumps( + validated, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None): + return { + "kind": kind, + "path": evidence_value("completed", path) if path is not None else evidence_value("unavailable"), + "line": evidence_value("completed", line) if line is not None else evidence_value("unavailable"), + "target": evidence_value("completed", target) if target is not None else evidence_value("unavailable"), + "fingerprint": evidence_value("completed", fingerprint) + if fingerprint is not None + else evidence_value("unavailable"), + } + + +def observe_entry_orientation(root, entry): + """Read bounded inert text evidence; never evaluate provider or MDX code.""" + if entry is None: + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + root = Path(root).absolute() + relative = normalize_repo_relative(entry, "entry") + path = safe_path(root / relative, root) + try: + if path.stat().st_size > 2 * 1024 * 1024: + raise ValueError("entry exceeds capacity") + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return { + "literal_h1": evidence_value("unavailable"), + "frontmatter_title": evidence_value("unavailable"), + "provider_rendered_title": evidence_value("unavailable"), + } + lines = text.removeprefix("\ufeff").splitlines() + in_fence = False + literal_h1 = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith(("```", "~~~")): + in_fence = not in_fence + continue + if not in_fence and re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + return { + "literal_h1": evidence_value("completed", literal_h1), + "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "provider_rendered_title": evidence_value("unavailable"), + } + + +def health_receipt(health): + """Copy existing score evidence without recalculation or reinterpretation.""" + if not isinstance(health, Mapping): + unavailable = evidence_value("not_assessed") + return { + "status": "not_assessed", + "rubric_version": unavailable.copy(), + "percentage": unavailable.copy(), + "earned_weight": unavailable.copy(), + "available_weight": unavailable.copy(), + "categories": {}, + "score_gates": { + "map_has_h1": unavailable.copy(), + "useful_entry": unavailable.copy(), + }, + } + categories = {} + for category, raw_fields in _CATEGORY_RAW_FIELDS.items(): + source = health.get("categories", {}).get(category) + if not isinstance(source, Mapping): + continue + raw = source.get("raw", {}) + categories[category] = { + "raw": { + field: evidence_value("completed", raw[field]) + if field in raw + else evidence_value("unavailable") + for field in raw_fields + }, + "earned": evidence_value("completed", source["earned"]), + "available": evidence_value("completed", source["available"]), + } + entry = health.get("categories", {}).get("entry", {}).get("raw", {}) + return { + "status": "completed", + "rubric_version": evidence_value("completed", health["rubric_version"]), + "percentage": evidence_value("completed", health["percentage"]), + "earned_weight": evidence_value("completed", health["earned_weight"]), + "available_weight": evidence_value("completed", health["available_weight"]), + "categories": categories, + "score_gates": { + "map_has_h1": evidence_value("completed", entry["map_has_h1"]) + if "map_has_h1" in entry + else evidence_value("unavailable"), + "useful_entry": evidence_value("completed", entry["useful_entry"]) + if "useful_entry" in entry + else evidence_value("unavailable"), + }, + } + + +def build_evidence_receipt( + *, + receipt_id, + repository_identifier, + commit, + checker_version, + run, + checker_payload, + orientation, + semantic, + unresolved=(), + doctor=None, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", +): + """Build one receipt from existing deterministic checker evidence.""" + checker_payload = _mapping(checker_payload, "checker payload") + navigation = checker_payload.get("navigation", {}) + health = health_receipt(checker_payload.get("health")) + measured = isinstance(checker_payload.get("health"), Mapping) + counts = { + "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "hidden_pages": len(navigation.get("hidden_pages", ())), + "redirects": len(navigation.get("redirects", ())), + } + categories = health.get("categories", {}) + for receipt_name, category, raw_name in ( + ("links_checked", "links", "checked"), + ("links_valid", "links", "valid"), + ("anchors_checked", "anchors", "checked"), + ("anchors_valid", "anchors", "valid"), + ): + source = categories.get(category, {}).get("raw", {}).get(raw_name) + counts[receipt_name] = source if source is not None else evidence_value("not_assessed") + for name in ("pages", "hidden_pages", "redirects"): + counts[name] = ( + evidence_value("completed", counts[name]) + if measured + else evidence_value("not_assessed") + ) + + findings = [] + for raw in checker_payload.get("findings", ()): + if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): + continue + path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + line = raw.get("line") if isinstance(raw.get("line"), int) else None + target = raw.get("target") if isinstance(raw.get("target"), str) else None + findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) + + semantic = _mapping(semantic, "semantic") + evaluator = semantic.get("evaluator", {}) + semantic_lane = { + "status": semantic["status"], + "evaluator": { + field: evaluator.get(field, evidence_value("not_assessed")) + for field in ("provider", "model", "version") + }, + "findings": list(semantic.get("findings", ())), + } + doctor = doctor or { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + receipt = { + "schema_version": EVIDENCE_RECEIPT_VERSION, + "receipt_id": receipt_id, + "repository": { + "identifier": repository_identifier, + "commit": evidence_value("completed", commit) + if commit is not None + else evidence_value("unavailable"), + }, + "checker": {"name": "diataxis-docs", "version": checker_version}, + "run": dict(run), + "surface": { + "provider": navigation.get("provider", "unknown"), + "authority": evidence_value("completed", navigation["authority"]) + if navigation.get("authority") is not None + else evidence_value("unavailable"), + "provider_root": evidence_value("completed", navigation["provider_root"]) + if navigation.get("provider_root") is not None + else evidence_value("unavailable"), + "entry": evidence_value("completed", navigation["entry"]) + if navigation.get("entry") is not None + else evidence_value("unavailable"), + }, + "counts": counts, + "orientation": dict(orientation), + "health": health, + "evidence": { + "deterministic": {"status": "completed", "findings": findings}, + "semantic": semantic_lane, + "unresolved": {"status": "completed", "candidates": list(unresolved)}, + }, + "doctor": dict(doctor), + "write_audit": { + "status": "completed", + "writes_attempted": evidence_value("completed", writes_attempted), + "writes_observed": evidence_value("completed", writes_observed), + }, + "git": { + "before": evidence_value("completed", git_before), + "after": evidence_value("completed", git_after), + }, + "unavailable_evidence": [], + } + receipt["unavailable_evidence"] = _collect_unavailable(receipt) + return validate_evidence_receipt(receipt) + + +def config_probe(path): + """Return bounded presence/digest evidence without parsing configuration code.""" + path = Path(path) + try: + size = path.stat().st_size + if size > 2 * 1024 * 1024: + raise ValueError("configuration exceeds capacity") + digest = hashlib.sha256(path.read_bytes()).hexdigest() + except (OSError, ValueError): + return {"status": "unavailable", "sha256": None, "bytes": None} + return {"status": "completed", "sha256": f"sha256:{digest}", "bytes": size} + + +__all__ = ( + "EVIDENCE_RECEIPT_VERSION", + "EVIDENCE_STATES", + "MAX_RECEIPT_BYTES", + "build_evidence_receipt", + "canonical_receipt_bytes", + "config_probe", + "evidence_value", + "finding_receipt", + "health_receipt", + "observe_entry_orientation", + "validate_evidence_receipt", +) diff --git a/skills/docs/scripts/_docs_checker/init_adoption.py b/skills/docs/scripts/_docs_checker/init_adoption.py index ec07ef2..96ba6dd 100644 --- a/skills/docs/scripts/_docs_checker/init_adoption.py +++ b/skills/docs/scripts/_docs_checker/init_adoption.py @@ -23,7 +23,7 @@ from .scan import discover_markdown, scan_documents -SKILL_VERSION = "0.1.3" +SKILL_VERSION = "0.1.4" def canonical_request_bytes(value): diff --git a/skills/docs/scripts/evidence_receipt.py b/skills/docs/scripts/evidence_receipt.py new file mode 100644 index 0000000..ce0fbb9 --- /dev/null +++ b/skills/docs/scripts/evidence_receipt.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Emit one sanitized Diataxis Docs evidence receipt to stdout.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from _docs_checker.evidence import ( + build_evidence_receipt, + canonical_receipt_bytes, + observe_entry_orientation, +) +from _docs_checker.health import health_summary +from _docs_checker.init_adoption import SKILL_VERSION +from check import check + + +_PARSER = argparse.ArgumentParser() +_PARSER.add_argument("root") +_PARSER.add_argument("--metadata-file", required=True) +_PARSER.add_argument("--map", default="docs/README.md") +_PARSER.add_argument("--scope", default="docs") + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError("repository Git evidence is unavailable") + return completed.stdout.strip() + + +def _status(root): + return "dirty" if _git(root, "status", "--porcelain=v1", "--untracked-files=all") else "clean" + + +def _metadata(path): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} + if not isinstance(value, dict) or set(value) != expected: + raise ValueError("metadata fields are invalid") + return value + + +def main(argv=None): + try: + namespace = _PARSER.parse_args(argv) + root = Path(namespace.root).absolute() + metadata = _metadata(namespace.metadata_file) + before_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + before = "dirty" if before_raw else "clean" + commit = _git(root, "rev-parse", "HEAD") + findings, _, measurements = check( + root, + map_path=namespace.map, + scope=namespace.scope, + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + health["surface"] = measurements["navigation"]["scope"] + health["provider"] = measurements["navigation"]["provider"] + after_raw = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + after = "dirty" if after_raw else "clean" + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + receipt = build_evidence_receipt( + receipt_id=metadata["receipt_id"], + repository_identifier=metadata["repository_identifier"], + commit=commit, + checker_version=SKILL_VERSION, + run=metadata["run"], + checker_payload=payload, + orientation=observe_entry_orientation(root, measurements["navigation"].get("entry")), + semantic=metadata["semantic"], + unresolved=metadata["unresolved"], + doctor=metadata["doctor"], + writes_attempted=0, + writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + git_before=before, + git_after=after, + ) + sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print( + json.dumps( + {"status": "failed", "error": str(exc), "receipt": None}, + ensure_ascii=True, + ) + ) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 8bc522d..c214a37 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -98,6 +98,7 @@ def test_canonical_checker_registry_covers_package_and_generated_bundles(self): } self.assertEqual(set(builder.CHECKER_FILES), canonical_scripts) self.assertIn("scripts/check.py", builder.CHECKER_FILES) + self.assertIn("scripts/evidence_receipt.py", builder.CHECKER_FILES) self.assertIn("scripts/init_closeout.py", builder.CHECKER_FILES) self.assertEqual( { @@ -116,6 +117,7 @@ def test_canonical_checker_registry_covers_package_and_generated_bundles(self): "discovery_policy.py", "surfaces.py", "receipt.py", + "evidence.py", "discovery_io.py", "discovery.py", "scan.py", @@ -437,7 +439,7 @@ def test_check_detects_version_drift_across_generated_surfaces(self): manifest["version"] = "9.9.9" manifest_path.write_text(json.dumps(manifest, sort_keys=True, indent=2) + "\n", encoding="utf-8") wrapper_path = out / "gemini/docs.md" - wrapper_path.write_text(wrapper_path.read_text(encoding="utf-8").replace("v0.1.3", "v9.9.9"), encoding="utf-8") + wrapper_path.write_text(wrapper_path.read_text(encoding="utf-8").replace("v0.1.4", "v9.9.9"), encoding="utf-8") check = subprocess.run( [sys.executable, str(BUILDER), "--check", "--output", str(out)], @@ -456,7 +458,7 @@ def test_check_detects_static_skill_version_drift(self): subprocess.run([sys.executable, str(BUILDER), "generate", "--output", str(out)], cwd=ROOT, check=True) skill_path = out / "copilot/SKILL.md" skill_path.write_text( - skill_path.read_text(encoding="utf-8").replace('version: "0.1.3"', 'version: "9.9.9"'), + skill_path.read_text(encoding="utf-8").replace('version: "0.1.4"', 'version: "9.9.9"'), encoding="utf-8", ) @@ -540,20 +542,20 @@ def test_generated_contracts(self): canonical = (ROOT / "skills/docs/SKILL.md").read_text(encoding="utf-8") for vendor in ("claude", "copilot", "grok", "cursor"): text = (adapter_skill_root(out, vendor) / "SKILL.md").read_text(encoding="utf-8") - self.assertIn(' version: "0.1.3"', text) + self.assertIn(' version: "0.1.4"', text) self.assertIn("user-invocable: true", text) self.assertIn("disable-model-invocation: true", text) self.assertEqual(text.split("---", 2)[-1].replace("\nuser-invocable: true\ndisable-model-invocation: true", "", 1), canonical.split("---", 2)[-1]) for vendor in ("gemini", "opencode"): wrapper = (out / vendor / "docs.md").read_text(encoding="utf-8") self.assertIn("docs", wrapper.lower()); self.assertIn("raw trailing text", wrapper.lower()) - self.assertIn("Diátaxis Docs v0.1.3", wrapper) + self.assertIn("Diátaxis Docs v0.1.4", wrapper) web = (out / "web" / "docs-help.txt").read_text(encoding="utf-8") self.assertIn("capabilit", web.lower()) - self.assertIn("Diátaxis Docs v0.1.3", web) + self.assertIn("Diátaxis Docs v0.1.4", web) manifest = json.loads((out / "plugin/.codex-plugin/plugin.json").read_text(encoding="utf-8")) self.assertEqual(manifest["name"], "statusnone-skills") - self.assertEqual(manifest["version"], "0.1.3") + self.assertEqual(manifest["version"], "0.1.4") self.assertEqual(manifest["interface"]["capabilities"], ["Read", "Write"]) self.assertEqual(manifest["interface"].get("brandColor"), "#6657E8") self.assertEqual(manifest["interface"].get("composerIcon"), "./assets/bounded-compass.png") diff --git a/tests/test_claude_distribution.py b/tests/test_claude_distribution.py index 425b60a..e48be1b 100644 --- a/tests/test_claude_distribution.py +++ b/tests/test_claude_distribution.py @@ -23,7 +23,7 @@ def test_marketplace_routes_to_generated_claude_adapter(self): self.assertEqual(plugin["name"], "diataxis-docs") self.assertEqual(plugin["displayName"], "Diátaxis Docs") self.assertEqual(plugin["source"], "./adapters/claude") - self.assertEqual(plugin["version"], "0.1.3") + self.assertEqual(plugin["version"], "0.1.4") source = PurePosixPath(plugin["source"]) self.assertEqual(source.parts[0], "adapters") @@ -47,7 +47,7 @@ def test_generated_claude_plugin_uses_the_canonical_version(self): self.assertEqual(manifest["description"], "Bounded repository memory. Evidence-backed documentation.") self.assertEqual(manifest["repository"], "https://github.com/Statusnone420/Skills") self.assertEqual(manifest["license"], "Apache-2.0") - self.assertEqual(manifest["version"], "0.1.3") + self.assertEqual(manifest["version"], "0.1.4") skill_root = output / "claude" / "skills" / "docs" self.assertFalse((output / "claude" / "SKILL.md").exists()) generated_skill = (skill_root / "SKILL.md").read_text(encoding="utf-8") diff --git a/tests/test_docs_checker_architecture.py b/tests/test_docs_checker_architecture.py index 58653bd..bacd5eb 100644 --- a/tests/test_docs_checker_architecture.py +++ b/tests/test_docs_checker_architecture.py @@ -23,6 +23,7 @@ "discovery_policy", "surfaces", "receipt", + "evidence", "discovery_io", "discovery", "scan", @@ -217,6 +218,7 @@ def test_internal_dependencies_are_acyclic_and_never_import_facade(self): graph["receipt"], {"continuation", "knowledge", "paths", "surfaces"}, ) + self.assertEqual(graph["evidence"], {"formats", "paths"}) self.assertEqual( graph["discovery_io"], {"discovery_policy", "formats", "metadata_io"}, diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py new file mode 100644 index 0000000..07a10f9 --- /dev/null +++ b/tests/test_docs_evidence.py @@ -0,0 +1,304 @@ +import copy +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).parents[1] +SCRIPTS = ROOT / "skills" / "docs" / "scripts" +TOOLS = ROOT / "tools" +sys.path.insert(0, str(SCRIPTS)) +sys.path.insert(0, str(TOOLS)) + +from _docs_checker import evidence +from _docs_checker.health import HEALTH_RUBRIC_VERSION, HEALTH_WEIGHTS +import prepare_docs_corpus +import run_docs_corpus + + +DOGFOOD = ROOT / "evals" / "dogfood" / "cline-0.1.3.json" +CORPUS = ROOT / "evals" / "docs-corpus-v1.json" +BASELINE = ROOT / "evals" / "docs-corpus-baseline-v1.json" +EVIDENCE_CLI = SCRIPTS / "evidence_receipt.py" +PINS = { + "cline": "d1837366c0b3a8cfa595e098e00c26275426fbc0", + "supabase": "c1d010a699a76738db63d22d33256b15bd7aea7a", + "docusaurus": "a0bc32214436d52a5ac9de9be1a515d872987366", + "vite": "e16ff3a1199293ac9cdfa6132c08fdea162215f3", + "uv": "bb9eba0bb4e04c08d8a09f1096ede335e54e5503", + "kubernetes-website": "5e1d1bde0ca03efe09608d59c573d6ec87052c24", +} + + +def _git(root, *args): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + check=True, + ) + return completed.stdout.strip() + + +def _fixture_checkout(workspace, spec, files): + root = workspace / spec["id"] + root.mkdir() + _git(root, "init") + _git(root, "config", "user.email", "docs-corpus@example.invalid") + _git(root, "config", "user.name", "Docs Corpus") + for relative, content in files.items(): + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + _git(root, "add", ".") + _git(root, "commit", "-m", "fixture") + commit = _git(root, "rev-parse", "HEAD") + _git(root, "remote", "add", "origin", spec["repository_url"]) + _git(root, "checkout", "--detach", commit) + spec["commit"] = commit + return root + + +class EvidenceReceiptTests(unittest.TestCase): + def setUp(self): + self.receipt = json.loads(DOGFOOD.read_text(encoding="utf-8")) + + def test_sanitized_dogfood_receipt_is_complete_and_explicit(self): + evidence.validate_evidence_receipt(self.receipt) + health = self.receipt["health"] + self.assertEqual(health["percentage"], {"status": "completed", "value": 29}) + self.assertFalse(health["score_gates"]["map_has_h1"]["value"]) + self.assertFalse(health["score_gates"]["useful_entry"]["value"]) + self.assertEqual(set(health["categories"]), set(HEALTH_WEIGHTS)) + self.assertEqual( + {name: row["available"]["value"] for name, row in health["categories"].items()}, + HEALTH_WEIGHTS, + ) + deterministic = self.receipt["evidence"]["deterministic"] + self.assertEqual(deterministic["status"], "completed") + self.assertEqual(len(deterministic["findings"]), 6) + self.assertEqual({row["kind"] for row in deterministic["findings"]}, {"missing-anchor"}) + semantic = self.receipt["evidence"]["semantic"] + self.assertEqual(semantic["status"], "completed") + self.assertEqual(semantic["findings"], []) + self.assertEqual(self.receipt["repository"]["commit"]["status"], "unavailable") + self.assertEqual(self.receipt["run"]["duration_seconds"]["value"], 508.747) + self.assertIn("repository.commit", self.receipt["unavailable_evidence"]) + + def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): + for mutation in ("unknown", "raw_transcript"): + value = copy.deepcopy(self.receipt) + value[mutation] = "not allowed" + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["path"] = { + "status": "completed", + "value": r"C:\private\notes.md", + } + with self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + + def test_unavailable_is_not_zero_and_must_match_index(self): + with self.assertRaises(ValueError): + evidence.evidence_value("unavailable", 0) + value = copy.deepcopy(self.receipt) + value["unavailable_evidence"] = [] + with self.assertRaisesRegex(ValueError, "unavailable_evidence"): + evidence.validate_evidence_receipt(value) + + def test_orientation_is_inert_and_does_not_change_rubric(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + entry = root / "docs" / "index.mdx" + entry.parent.mkdir() + entry.write_text( + "---\ntitle: Rendered title\n---\n\n```js\n# not a heading\nthrow new Error('never execute')\n```\n\n## Start\n", + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, "docs/index.mdx") + self.assertEqual(observed["literal_h1"], {"status": "completed", "value": False}) + self.assertEqual(observed["frontmatter_title"], {"status": "completed", "value": True}) + self.assertEqual(observed["provider_rendered_title"]["status"], "unavailable") + self.assertEqual(HEALTH_RUBRIC_VERSION, 2) + self.assertEqual( + HEALTH_WEIGHTS, + {"entry": 20, "path_safety": 15, "links": 20, "anchors": 10, "reachability": 25, "titles": 10}, + ) + + def test_stdout_receipt_entrypoint_combines_one_checker_run(self): + with tempfile.TemporaryDirectory() as td: + base = Path(td) + root = base / "repo" + root.mkdir() + _git(root, "init") + _git(root, "config", "user.email", "receipt@example.invalid") + _git(root, "config", "user.name", "Receipt Fixture") + docs = root / "docs" + docs.mkdir() + (docs / "README.md").write_text("# Home\n\n## Start\n", encoding="utf-8") + _git(root, "add", ".") + _git(root, "commit", "-m", "fixture") + metadata = base / "metadata.json" + metadata.write_text( + json.dumps( + { + "receipt_id": "fixture-receipt", + "repository_identifier": "example.invalid/docs/fixture", + "run": { + "id": "fixture-run", + "client": "test-harness", + "model_provider": "local", + "model": "deterministic-harness", + "effort": "not-applicable", + "turns": {"status": "completed", "value": 1}, + "duration_seconds": {"status": "not_assessed", "value": None}, + "commands": ["docs-check"], + }, + "semantic": run_docs_corpus._semantic_not_assessed(), + "unresolved": [], + "doctor": { + "status": "not_assessed", + "treatment_fingerprint": evidence.evidence_value("not_assessed"), + "approval_line_present": evidence.evidence_value("not_assessed"), + }, + } + ), + encoding="utf-8", + ) + completed = subprocess.run( + [ + sys.executable, + "-B", + str(EVIDENCE_CLI), + str(root), + "--metadata-file", + str(metadata), + ], + cwd=ROOT, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr) + receipt = json.loads(completed.stdout) + evidence.validate_evidence_receipt(receipt) + self.assertEqual(receipt["health"]["status"], "completed") + self.assertEqual(receipt["write_audit"]["writes_observed"]["value"], 0) + + +class CorpusHarnessTests(unittest.TestCase): + def test_manifest_has_six_exact_immutable_pins(self): + manifest = run_docs_corpus.load_manifest(CORPUS) + self.assertEqual({row["id"]: row["commit"] for row in manifest["repositories"]}, PINS) + self.assertEqual( + {row["provider"] for row in manifest["repositories"]}, + {"mintlify", "custom-mdx", "docusaurus", "vitepress", "mkdocs", "hugo"}, + ) + for row in manifest["repositories"]: + self.assertTrue(row["entry"]) + self.assertTrue(row["authority_probes"]) + self.assertTrue(row["config_probes"]) + self.assertTrue(row["sparse_paths"]) + + def test_checked_in_baseline_separates_supported_and_unavailable_evidence(self): + baseline = json.loads(BASELINE.read_text(encoding="utf-8")) + self.assertEqual(baseline["checker_version"], "0.1.4") + self.assertEqual(baseline["rubric"]["changed"], False) + rows = {row["id"]: row for row in baseline["repositories"]} + self.assertEqual(set(rows), set(PINS)) + for row in rows.values(): + evidence.validate_evidence_receipt(row["receipt"]) + self.assertTrue(all(item["status"] == "completed" for item in row["configurations"])) + self.assertEqual(rows["cline"]["receipt"]["health"]["percentage"]["value"], 29) + for repository_id in set(rows) - {"cline"}: + health = rows[repository_id]["receipt"]["health"] + self.assertEqual(health["status"], "not_assessed") + self.assertIsNone(health["percentage"]["value"]) + + def test_unsupported_provider_is_observed_but_not_scored_or_executed(self): + with tempfile.TemporaryDirectory() as td: + workspace = Path(td) + spec = { + "id": "vite", + "repository_url": "https://github.com/vitejs/vite.git", + "commit": "0" * 40, + "provider": "vitepress", + "measurement": "unsupported", + "scope": "docs", + "entry": "docs/index.md", + "authority_probes": ["docs/.vitepress/config.ts"], + "config_probes": ["docs/.vitepress/config.ts"], + "sparse_paths": ["docs"], + } + root = _fixture_checkout( + workspace, + spec, + { + "docs/index.md": "---\ntitle: Vite\n---\n\n", + "docs/.vitepress/config.ts": "throw new Error('never execute')\n", + }, + ) + before = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + result = run_docs_corpus.run_repository(workspace, spec) + after = _git(root, "status", "--porcelain=v1", "--untracked-files=all") + self.assertEqual(before, after) + self.assertEqual(result["receipt"]["health"]["status"], "not_assessed") + self.assertEqual(result["receipt"]["health"]["percentage"]["value"], None) + self.assertEqual(result["receipt"]["counts"]["pages"]["status"], "not_assessed") + self.assertEqual(result["configurations"][0]["status"], "completed") + self.assertTrue(result["configurations"][0]["sha256"].startswith("sha256:")) + + def test_checkout_verification_rejects_branch_dirty_and_wrong_commit(self): + with tempfile.TemporaryDirectory() as td: + workspace = Path(td) + spec = { + "id": "uv", + "repository_url": "https://github.com/astral-sh/uv.git", + "commit": "0" * 40, + "provider": "mkdocs", + "measurement": "unsupported", + "scope": "docs", + "entry": "docs/index.md", + "authority_probes": ["mkdocs.yml"], + "config_probes": ["mkdocs.yml"], + "sparse_paths": ["docs", "mkdocs.yml"], + } + root = _fixture_checkout( + workspace, + spec, + {"docs/index.md": "# uv\n", "mkdocs.yml": "site_name: uv\n"}, + ) + good_commit = spec["commit"] + spec["commit"] = "f" * 40 + with self.assertRaisesRegex(ValueError, "commit mismatch"): + run_docs_corpus.verify_checkout(workspace, spec) + spec["commit"] = good_commit + (root / "dirty.txt").write_text("dirty\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "dirty"): + run_docs_corpus.verify_checkout(workspace, spec) + (root / "dirty.txt").unlink() + _git(root, "switch", "-c", "fixture-branch") + with self.assertRaisesRegex(ValueError, "not detached"): + run_docs_corpus.verify_checkout(workspace, spec) + + def test_prepare_refuses_unowned_or_existing_workspace(self): + with tempfile.TemporaryDirectory() as td: + original = prepare_docs_corpus.WORKSPACE_ROOT + try: + prepare_docs_corpus.WORKSPACE_ROOT = Path(td) + unowned = Path(td) / "unowned" + unowned.mkdir() + (unowned / "keep.txt").write_text("keep\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "unowned"): + prepare_docs_corpus._workspace(unowned, CORPUS, "docs-corpus-v1") + self.assertEqual((unowned / "keep.txt").read_text(encoding="utf-8"), "keep\n") + finally: + prepare_docs_corpus.WORKSPACE_ROOT = original + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_docs_skill.py b/tests/test_docs_skill.py index 3544101..c773de3 100644 --- a/tests/test_docs_skill.py +++ b/tests/test_docs_skill.py @@ -117,7 +117,7 @@ def test_canonical_public_alpha_version_and_help_identity(self): skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") commands = (SKILL / "references" / "commands.md").read_text(encoding="utf-8") - self.assertIn("metadata:\n author: Statusnone\n version: \"0.1.3\"", skill) + self.assertIn("metadata:\n author: Statusnone\n version: \"0.1.4\"", skill) self.assertIn("Diátaxis Docs v", commands) def test_default_help_uses_plain_english_daily_commands(self): @@ -370,10 +370,10 @@ def test_doctor_evals_cover_exhaustive_scoped_and_fingerprinted_treatments(self) def test_canonical_version_is_strict_semver(self): skill = (SKILL / "SKILL.md").read_text(encoding="utf-8") - self.assertEqual(build_adapters.canonical_version(skill), "0.1.3") + self.assertEqual(build_adapters.canonical_version(skill), "0.1.4") for invalid in ("1", "v0.1.0", "01.0.0", "0.1.0-alpha"): with self.subTest(invalid=invalid): - malformed = skill.replace('version: "0.1.3"', f'version: "{invalid}"') + malformed = skill.replace('version: "0.1.4"', f'version: "{invalid}"') with self.assertRaises(ValueError): build_adapters.canonical_version(malformed) diff --git a/tools/build_adapters.py b/tools/build_adapters.py index 1161c66..4395c50 100644 --- a/tools/build_adapters.py +++ b/tools/build_adapters.py @@ -11,6 +11,7 @@ ASSETS = ("bounded-compass-small.svg", "bounded-compass.png") CHECKER_FILES = ( "scripts/check.py", + "scripts/evidence_receipt.py", "scripts/init_closeout.py", "scripts/doctor_closeout.py", "scripts/_docs_checker/__init__.py", @@ -23,6 +24,7 @@ "scripts/_docs_checker/discovery_policy.py", "scripts/_docs_checker/surfaces.py", "scripts/_docs_checker/receipt.py", + "scripts/_docs_checker/evidence.py", "scripts/_docs_checker/discovery_io.py", "scripts/_docs_checker/discovery.py", "scripts/_docs_checker/scan.py", diff --git a/tools/prepare_docs_corpus.py b/tools/prepare_docs_corpus.py new file mode 100644 index 0000000..2181e6e --- /dev/null +++ b/tools/prepare_docs_corpus.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Opt-in, non-destructive acquisition for the pinned documentation corpus.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +from pathlib import Path + +from run_docs_corpus import ( + DEFAULT_MANIFEST, + DEFAULT_WORKSPACE, + ROOT, + load_manifest, + verify_checkout, +) + + +WORKSPACE_ROOT = ROOT / "evals" / "workspace" +MARKER = ".docs-corpus-owner.json" + + +def _run(command, *, input_text=None): + completed = subprocess.run( + command, + input=input_text, + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode: + raise ValueError(f"corpus acquisition failed: {' '.join(command[:4])}") + return completed + + +def _workspace(path, manifest_path, corpus_id): + root = WORKSPACE_ROOT.absolute() + workspace = Path(path).absolute() + if Path(root, "sentinel").parent != root: + raise ValueError("workspace root is invalid") + try: + workspace.relative_to(root) + except ValueError as exc: + raise ValueError("corpus workspace must remain under evals/workspace") from exc + if workspace == root: + raise ValueError("corpus workspace must be a named child") + root.mkdir(parents=True, exist_ok=True) + if workspace.exists() and not workspace.is_dir(): + raise ValueError("corpus workspace is not a directory") + workspace.mkdir(parents=True, exist_ok=True) + marker = workspace / MARKER + digest = hashlib.sha256(Path(manifest_path).read_bytes()).hexdigest() + expected = {"corpus_id": corpus_id, "manifest_sha256": f"sha256:{digest}"} + if marker.exists(): + try: + current = json.loads(marker.read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("corpus workspace marker is malformed") from exc + if current != expected: + raise ValueError("corpus workspace marker does not match") + else: + if any(workspace.iterdir()): + raise ValueError("refusing to reuse an unowned corpus workspace") + marker.write_text( + json.dumps(expected, sort_keys=True, separators=(",", ":")) + "\n", + encoding="utf-8", + newline="\n", + ) + return workspace + + +def prepare(manifest=DEFAULT_MANIFEST, workspace=DEFAULT_WORKSPACE, repository_ids=()): + manifest_value = load_manifest(manifest) + workspace = _workspace(workspace, manifest, manifest_value["corpus_id"]) + selected = set(repository_ids) + known = {spec["id"] for spec in manifest_value["repositories"]} + if selected - known: + raise ValueError("unknown corpus repository selection") + rows = [] + for spec in manifest_value["repositories"]: + if selected and spec["id"] not in selected: + continue + target = workspace / spec["id"] + if target.exists(): + raise ValueError(f"refusing to update or reuse corpus repository: {spec['id']}") + _run(["git", "init", str(target)]) + _run(["git", "-C", str(target), "remote", "add", "origin", spec["repository_url"]]) + _run(["git", "-C", str(target), "sparse-checkout", "init", "--no-cone"]) + _run( + ["git", "-C", str(target), "sparse-checkout", "set", "--no-cone", "--stdin"], + input_text="\n".join(spec["sparse_paths"]) + "\n", + ) + _run( + [ + "git", + "-C", + str(target), + "fetch", + "--depth=1", + "--filter=blob:none", + "origin", + spec["commit"], + ] + ) + _run(["git", "-C", str(target), "checkout", "--detach", "FETCH_HEAD"]) + verify_checkout(workspace, spec) + rows.append({"id": spec["id"], "commit": spec["commit"], "status": "prepared"}) + return {"status": "completed", "corpus_id": manifest_value["corpus_id"], "repositories": rows} + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", default=str(DEFAULT_MANIFEST)) + parser.add_argument("--workspace", default=str(DEFAULT_WORKSPACE)) + parser.add_argument("--repository", action="append", default=[]) + namespace = parser.parse_args(argv) + try: + print( + json.dumps( + prepare(namespace.manifest, namespace.workspace, namespace.repository), + indent=2, + sort_keys=True, + ensure_ascii=True, + ) + ) + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=True)) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/run_docs_corpus.py b/tools/run_docs_corpus.py new file mode 100644 index 0000000..a072e2b --- /dev/null +++ b/tools/run_docs_corpus.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +"""Run the pinned documentation corpus without modifying target repositories.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "skills" / "docs" / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from _docs_checker.evidence import ( # noqa: E402 + build_evidence_receipt, + config_probe, + evidence_value, + observe_entry_orientation, +) +from _docs_checker.health import HEALTH_RUBRIC_VERSION, HEALTH_WEIGHTS, health_summary # noqa: E402 +from _docs_checker.init_adoption import SKILL_VERSION # noqa: E402 +from _docs_checker.paths import _assert_no_reparse_components, normalize_repo_relative, safe_path # noqa: E402 +from check import check # noqa: E402 + + +DEFAULT_MANIFEST = ROOT / "evals" / "docs-corpus-v1.json" +DEFAULT_WORKSPACE = ROOT / "evals" / "workspace" / "docs-corpus-v1" +_SHA = re.compile(r"^[0-9a-f]{40}$") +_ID = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$") +_PROVIDERS = frozenset({"mintlify", "custom-mdx", "docusaurus", "vitepress", "mkdocs", "hugo"}) + + +def _git(root, *args, allowed=(0,)): + completed = subprocess.run( + ["git", "-C", str(root), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="strict", + check=False, + ) + if completed.returncode not in allowed: + raise ValueError(f"Git evidence failed: {' '.join(args)}") + return completed + + +def _relative(value, name): + normalized = normalize_repo_relative(value, name) + if normalized != value: + raise ValueError(f"{name} must be normalized") + return normalized + + +def load_manifest(path=DEFAULT_MANIFEST): + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError("corpus manifest is unavailable or malformed") from exc + if not isinstance(value, dict) or set(value) != {"schema_version", "corpus_id", "repositories"}: + raise ValueError("corpus manifest fields are invalid") + if value["schema_version"] != 1 or value["corpus_id"] != "docs-corpus-v1": + raise ValueError("corpus manifest identity is invalid") + repositories = value["repositories"] + if not isinstance(repositories, list) or len(repositories) != 6: + raise ValueError("corpus manifest must contain six repositories") + expected_fields = { + "id", + "repository_url", + "commit", + "provider", + "measurement", + "scope", + "entry", + "authority_probes", + "config_probes", + "sparse_paths", + } + seen = set() + for index, spec in enumerate(repositories): + name = f"repositories[{index}]" + if not isinstance(spec, dict) or set(spec) != expected_fields: + raise ValueError(f"{name} fields are invalid") + if not isinstance(spec["id"], str) or _ID.fullmatch(spec["id"]) is None: + raise ValueError(f"{name}.id is invalid") + if spec["id"] in seen: + raise ValueError("corpus repository IDs must be unique") + seen.add(spec["id"]) + if not isinstance(spec["repository_url"], str) or not re.fullmatch( + r"https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\.git", + spec["repository_url"], + ): + raise ValueError(f"{name}.repository_url is invalid") + if not isinstance(spec["commit"], str) or _SHA.fullmatch(spec["commit"]) is None: + raise ValueError(f"{name}.commit is invalid") + if spec["provider"] not in _PROVIDERS: + raise ValueError(f"{name}.provider is invalid") + expected_measurement = "supported" if spec["provider"] == "mintlify" else "unsupported" + if spec["measurement"] != expected_measurement: + raise ValueError(f"{name}.measurement is invalid") + _relative(spec["scope"], f"{name}.scope") + _relative(spec["entry"], f"{name}.entry") + for field in ("authority_probes", "config_probes", "sparse_paths"): + paths = spec[field] + if not isinstance(paths, list) or not paths or len(paths) != len(set(paths)): + raise ValueError(f"{name}.{field} is invalid") + for path_index, path in enumerate(paths): + _relative(path, f"{name}.{field}[{path_index}]") + return value + + +def _repo_path(workspace, repository_id): + workspace = Path(workspace).absolute() + _assert_no_reparse_components(workspace) + candidate = safe_path(workspace / repository_id, workspace) + _assert_no_reparse_components(candidate) + if candidate.is_symlink() or ( + os.name == "nt" + and getattr(candidate.lstat(), "st_file_attributes", 0) & 0x400 + ): + raise ValueError("corpus repository cannot be a reparse point") + return candidate + + +def verify_checkout(workspace, spec): + path = _repo_path(workspace, spec["id"]) + if not path.is_dir(): + raise ValueError(f"corpus repository is missing: {spec['id']}") + head = _git(path, "rev-parse", "HEAD").stdout.strip() + if head != spec["commit"]: + raise ValueError(f"corpus commit mismatch: {spec['id']}") + symbolic = _git(path, "symbolic-ref", "-q", "HEAD", allowed=(0, 1)) + if symbolic.returncode == 0: + raise ValueError(f"corpus checkout is not detached: {spec['id']}") + remote = _git(path, "remote", "get-url", "origin").stdout.strip() + if remote != spec["repository_url"]: + raise ValueError(f"corpus remote mismatch: {spec['id']}") + status = _git(path, "status", "--porcelain=v1", "--untracked-files=all").stdout + if status: + raise ValueError(f"corpus checkout is dirty: {spec['id']}") + entry = safe_path(path / spec["entry"], path) + if not entry.is_file(): + raise ValueError(f"corpus entry is missing: {spec['id']}") + for relative in {*spec["authority_probes"], *spec["config_probes"]}: + if not safe_path(path / relative, path).is_file(): + raise ValueError(f"corpus probe is missing: {spec['id']}:{relative}") + return path, status + + +def _semantic_not_assessed(): + return { + "status": "not_assessed", + "evaluator": { + "provider": evidence_value("not_assessed"), + "model": evidence_value("not_assessed"), + "version": evidence_value("not_assessed"), + }, + "findings": [], + } + + +def _unsupported_payload(spec): + return { + "navigation": { + "status": "unmeasured", + "provider": spec["provider"], + "authority": spec["authority_probes"][0], + "provider_root": spec["scope"], + "entry": spec["entry"], + "navigated_pages": [], + "hidden_pages": [], + "redirects": [], + }, + "findings": [], + } + + +def run_repository(workspace, spec): + path, before_raw = verify_checkout(workspace, spec) + configurations = [] + for relative in spec["config_probes"]: + probe = config_probe(safe_path(path / relative, path)) + if probe["status"] != "completed": + raise ValueError(f"corpus configuration became unavailable: {spec['id']}:{relative}") + configurations.append({"path": relative, **probe}) + + if spec["measurement"] == "supported": + findings, _, measurements = check( + path, + map_path=spec["entry"], + scope=spec["scope"], + _measurements=True, + ) + health = health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + payload = { + "navigation": measurements["navigation"], + "health": health, + "findings": findings, + } + unresolved = [] + else: + payload = _unsupported_payload(spec) + unresolved = [ + {"kind": f"unsupported-{spec['provider']}-semantics", "status": "not_assessed"} + ] + + after_raw = _git(path, "status", "--porcelain=v1", "--untracked-files=all").stdout + if after_raw != before_raw: + raise ValueError(f"corpus runner modified target repository: {spec['id']}") + receipt = build_evidence_receipt( + receipt_id=f"docs-corpus-v1-{spec['id']}", + repository_identifier=spec["repository_url"].removeprefix("https://").removesuffix(".git"), + commit=spec["commit"], + checker_version=SKILL_VERSION, + run={ + "id": f"docs-corpus-v1-{spec['id']}", + "client": "local-corpus-harness", + "model_provider": "local", + "model": "deterministic-harness", + "effort": "not-applicable", + "turns": evidence_value("completed", 1), + "duration_seconds": evidence_value("not_assessed"), + "commands": ["corpus-check"], + }, + checker_payload=payload, + orientation=observe_entry_orientation(path, spec["entry"]), + semantic=_semantic_not_assessed(), + unresolved=unresolved, + writes_attempted=0, + writes_observed=0, + git_before="clean", + git_after="clean", + ) + return { + "id": spec["id"], + "commit": spec["commit"], + "provider": spec["provider"], + "measurement": spec["measurement"], + "configurations": configurations, + "receipt": receipt, + } + + +def run_corpus(manifest=DEFAULT_MANIFEST, workspace=DEFAULT_WORKSPACE): + manifest_value = load_manifest(manifest) + workspace = Path(workspace).absolute() + if not workspace.is_dir(): + raise ValueError("corpus workspace is missing") + rows = [run_repository(workspace, spec) for spec in manifest_value["repositories"]] + return { + "schema_version": 1, + "corpus_id": manifest_value["corpus_id"], + "checker_version": SKILL_VERSION, + "rubric": { + "version": HEALTH_RUBRIC_VERSION, + "weights": dict(HEALTH_WEIGHTS), + "changed": False, + }, + "repositories": rows, + } + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", default=str(DEFAULT_MANIFEST)) + parser.add_argument("--workspace", default=str(DEFAULT_WORKSPACE)) + parser.add_argument("--output") + namespace = parser.parse_args(argv) + try: + result = run_corpus(namespace.manifest, namespace.workspace) + encoded = json.dumps(result, indent=2, sort_keys=True, ensure_ascii=True) + "\n" + if namespace.output: + Path(namespace.output).write_text(encoded, encoding="utf-8", newline="\n") + else: + print(encoded, end="") + return 0 + except (OSError, UnicodeError, ValueError) as exc: + print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=True)) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) From d5dcb782ce05c72cc4cffe246b7dad92f2253a21 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 02:20:38 -0400 Subject: [PATCH 02/20] Harden 0.1.4 evidence boundaries --- .../docs/scripts/_docs_checker/evidence.py | 75 ++++++++++-- .../copilot/scripts/_docs_checker/evidence.py | 75 ++++++++++-- .../cursor/scripts/_docs_checker/evidence.py | 75 ++++++++++-- .../grok/scripts/_docs_checker/evidence.py | 75 ++++++++++-- .../docs/scripts/_docs_checker/evidence.py | 75 ++++++++++-- evals/docs-corpus-baseline-v1.json | 2 +- skills/docs/scripts/_docs_checker/evidence.py | 75 ++++++++++-- tests/test_docs_evidence.py | 112 ++++++++++++++++++ tools/prepare_docs_corpus.py | 18 +-- tools/run_docs_corpus.py | 25 +++- 10 files changed, 540 insertions(+), 67 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 8c86091..431ae79 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -12,6 +12,7 @@ import re from collections.abc import Mapping, Sequence from pathlib import Path +from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars from .paths import normalize_repo_relative, safe_path @@ -30,7 +31,21 @@ r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", re.IGNORECASE, ) -_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_WINDOWS_ABSOLUTE = re.compile(r"(? 100: + raise ValueError(f"{name} must not exceed 100") + return value + + def _evidence(value, name, *, validator=None): value = _exact_keys(value, {"status", "value"}, name) status = value["status"] @@ -172,8 +206,16 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - if value.startswith(("http://", "https://", "file:")): - raise ValueError(f"{name} must be a local route") + current = value + for _ in range(4): + if _URI_SCHEME.search(current): + raise ValueError(f"{name} must be a local route") + if _CREDENTIAL_PARAMETER.search(current): + raise ValueError(f"{name} exposes credential-shaped data") + decoded = unquote(current) + if decoded == current: + break + current = decoded return value @@ -260,10 +302,12 @@ def _validate_unresolved(value): raise ValueError("unresolved candidate status is invalid") -def _validate_categories(value): +def _validate_categories(value, *, required=False): value = _mapping(value, "health.categories") if set(value) - set(_CATEGORY_RAW_FIELDS): raise ValueError("health.categories contains an unknown category") + if required and set(value) != set(_CATEGORY_RAW_FIELDS): + raise ValueError("completed health requires every category") for category, fields in _CATEGORY_RAW_FIELDS.items(): if category not in value: continue @@ -283,6 +327,12 @@ def _validate_categories(value): _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + if ( + row["earned"]["status"] == "completed" + and row["available"]["status"] == "completed" + and row["earned"]["value"] > row["available"]["value"] + ): + raise ValueError(f"health.categories.{category}.earned exceeds available") def _collect_unavailable(value, path=""): @@ -355,9 +405,16 @@ def validate_evidence_receipt(value): if health["status"] not in EVIDENCE_STATES: raise ValueError("health.status is invalid") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) - for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health["percentage"], "health.percentage", validator=_percentage) + for field in ("earned_weight", "available_weight"): _evidence(health[field], f"health.{field}", validator=_nonnegative_number) - _validate_categories(health["categories"]) + if ( + health["earned_weight"]["status"] == "completed" + and health["available_weight"]["status"] == "completed" + and health["earned_weight"]["value"] > health["available_weight"]["value"] + ): + raise ValueError("health.earned_weight exceeds available_weight") + _validate_categories(health["categories"], required=health["status"] == "completed") gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") for field in gates: _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) @@ -555,7 +612,7 @@ def build_evidence_receipt( health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { - "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "pages": len(navigation.get("navigated_pages", ())), "hidden_pages": len(navigation.get("hidden_pages", ())), "redirects": len(navigation.get("redirects", ())), } diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 8c86091..431ae79 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -12,6 +12,7 @@ import re from collections.abc import Mapping, Sequence from pathlib import Path +from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars from .paths import normalize_repo_relative, safe_path @@ -30,7 +31,21 @@ r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", re.IGNORECASE, ) -_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_WINDOWS_ABSOLUTE = re.compile(r"(? 100: + raise ValueError(f"{name} must not exceed 100") + return value + + def _evidence(value, name, *, validator=None): value = _exact_keys(value, {"status", "value"}, name) status = value["status"] @@ -172,8 +206,16 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - if value.startswith(("http://", "https://", "file:")): - raise ValueError(f"{name} must be a local route") + current = value + for _ in range(4): + if _URI_SCHEME.search(current): + raise ValueError(f"{name} must be a local route") + if _CREDENTIAL_PARAMETER.search(current): + raise ValueError(f"{name} exposes credential-shaped data") + decoded = unquote(current) + if decoded == current: + break + current = decoded return value @@ -260,10 +302,12 @@ def _validate_unresolved(value): raise ValueError("unresolved candidate status is invalid") -def _validate_categories(value): +def _validate_categories(value, *, required=False): value = _mapping(value, "health.categories") if set(value) - set(_CATEGORY_RAW_FIELDS): raise ValueError("health.categories contains an unknown category") + if required and set(value) != set(_CATEGORY_RAW_FIELDS): + raise ValueError("completed health requires every category") for category, fields in _CATEGORY_RAW_FIELDS.items(): if category not in value: continue @@ -283,6 +327,12 @@ def _validate_categories(value): _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + if ( + row["earned"]["status"] == "completed" + and row["available"]["status"] == "completed" + and row["earned"]["value"] > row["available"]["value"] + ): + raise ValueError(f"health.categories.{category}.earned exceeds available") def _collect_unavailable(value, path=""): @@ -355,9 +405,16 @@ def validate_evidence_receipt(value): if health["status"] not in EVIDENCE_STATES: raise ValueError("health.status is invalid") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) - for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health["percentage"], "health.percentage", validator=_percentage) + for field in ("earned_weight", "available_weight"): _evidence(health[field], f"health.{field}", validator=_nonnegative_number) - _validate_categories(health["categories"]) + if ( + health["earned_weight"]["status"] == "completed" + and health["available_weight"]["status"] == "completed" + and health["earned_weight"]["value"] > health["available_weight"]["value"] + ): + raise ValueError("health.earned_weight exceeds available_weight") + _validate_categories(health["categories"], required=health["status"] == "completed") gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") for field in gates: _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) @@ -555,7 +612,7 @@ def build_evidence_receipt( health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { - "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "pages": len(navigation.get("navigated_pages", ())), "hidden_pages": len(navigation.get("hidden_pages", ())), "redirects": len(navigation.get("redirects", ())), } diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 8c86091..431ae79 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -12,6 +12,7 @@ import re from collections.abc import Mapping, Sequence from pathlib import Path +from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars from .paths import normalize_repo_relative, safe_path @@ -30,7 +31,21 @@ r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", re.IGNORECASE, ) -_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_WINDOWS_ABSOLUTE = re.compile(r"(? 100: + raise ValueError(f"{name} must not exceed 100") + return value + + def _evidence(value, name, *, validator=None): value = _exact_keys(value, {"status", "value"}, name) status = value["status"] @@ -172,8 +206,16 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - if value.startswith(("http://", "https://", "file:")): - raise ValueError(f"{name} must be a local route") + current = value + for _ in range(4): + if _URI_SCHEME.search(current): + raise ValueError(f"{name} must be a local route") + if _CREDENTIAL_PARAMETER.search(current): + raise ValueError(f"{name} exposes credential-shaped data") + decoded = unquote(current) + if decoded == current: + break + current = decoded return value @@ -260,10 +302,12 @@ def _validate_unresolved(value): raise ValueError("unresolved candidate status is invalid") -def _validate_categories(value): +def _validate_categories(value, *, required=False): value = _mapping(value, "health.categories") if set(value) - set(_CATEGORY_RAW_FIELDS): raise ValueError("health.categories contains an unknown category") + if required and set(value) != set(_CATEGORY_RAW_FIELDS): + raise ValueError("completed health requires every category") for category, fields in _CATEGORY_RAW_FIELDS.items(): if category not in value: continue @@ -283,6 +327,12 @@ def _validate_categories(value): _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + if ( + row["earned"]["status"] == "completed" + and row["available"]["status"] == "completed" + and row["earned"]["value"] > row["available"]["value"] + ): + raise ValueError(f"health.categories.{category}.earned exceeds available") def _collect_unavailable(value, path=""): @@ -355,9 +405,16 @@ def validate_evidence_receipt(value): if health["status"] not in EVIDENCE_STATES: raise ValueError("health.status is invalid") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) - for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health["percentage"], "health.percentage", validator=_percentage) + for field in ("earned_weight", "available_weight"): _evidence(health[field], f"health.{field}", validator=_nonnegative_number) - _validate_categories(health["categories"]) + if ( + health["earned_weight"]["status"] == "completed" + and health["available_weight"]["status"] == "completed" + and health["earned_weight"]["value"] > health["available_weight"]["value"] + ): + raise ValueError("health.earned_weight exceeds available_weight") + _validate_categories(health["categories"], required=health["status"] == "completed") gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") for field in gates: _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) @@ -555,7 +612,7 @@ def build_evidence_receipt( health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { - "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "pages": len(navigation.get("navigated_pages", ())), "hidden_pages": len(navigation.get("hidden_pages", ())), "redirects": len(navigation.get("redirects", ())), } diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 8c86091..431ae79 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -12,6 +12,7 @@ import re from collections.abc import Mapping, Sequence from pathlib import Path +from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars from .paths import normalize_repo_relative, safe_path @@ -30,7 +31,21 @@ r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", re.IGNORECASE, ) -_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_WINDOWS_ABSOLUTE = re.compile(r"(? 100: + raise ValueError(f"{name} must not exceed 100") + return value + + def _evidence(value, name, *, validator=None): value = _exact_keys(value, {"status", "value"}, name) status = value["status"] @@ -172,8 +206,16 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - if value.startswith(("http://", "https://", "file:")): - raise ValueError(f"{name} must be a local route") + current = value + for _ in range(4): + if _URI_SCHEME.search(current): + raise ValueError(f"{name} must be a local route") + if _CREDENTIAL_PARAMETER.search(current): + raise ValueError(f"{name} exposes credential-shaped data") + decoded = unquote(current) + if decoded == current: + break + current = decoded return value @@ -260,10 +302,12 @@ def _validate_unresolved(value): raise ValueError("unresolved candidate status is invalid") -def _validate_categories(value): +def _validate_categories(value, *, required=False): value = _mapping(value, "health.categories") if set(value) - set(_CATEGORY_RAW_FIELDS): raise ValueError("health.categories contains an unknown category") + if required and set(value) != set(_CATEGORY_RAW_FIELDS): + raise ValueError("completed health requires every category") for category, fields in _CATEGORY_RAW_FIELDS.items(): if category not in value: continue @@ -283,6 +327,12 @@ def _validate_categories(value): _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + if ( + row["earned"]["status"] == "completed" + and row["available"]["status"] == "completed" + and row["earned"]["value"] > row["available"]["value"] + ): + raise ValueError(f"health.categories.{category}.earned exceeds available") def _collect_unavailable(value, path=""): @@ -355,9 +405,16 @@ def validate_evidence_receipt(value): if health["status"] not in EVIDENCE_STATES: raise ValueError("health.status is invalid") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) - for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health["percentage"], "health.percentage", validator=_percentage) + for field in ("earned_weight", "available_weight"): _evidence(health[field], f"health.{field}", validator=_nonnegative_number) - _validate_categories(health["categories"]) + if ( + health["earned_weight"]["status"] == "completed" + and health["available_weight"]["status"] == "completed" + and health["earned_weight"]["value"] > health["available_weight"]["value"] + ): + raise ValueError("health.earned_weight exceeds available_weight") + _validate_categories(health["categories"], required=health["status"] == "completed") gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") for field in gates: _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) @@ -555,7 +612,7 @@ def build_evidence_receipt( health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { - "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "pages": len(navigation.get("navigated_pages", ())), "hidden_pages": len(navigation.get("hidden_pages", ())), "redirects": len(navigation.get("redirects", ())), } diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 8c86091..431ae79 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -12,6 +12,7 @@ import re from collections.abc import Mapping, Sequence from pathlib import Path +from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars from .paths import normalize_repo_relative, safe_path @@ -30,7 +31,21 @@ r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", re.IGNORECASE, ) -_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_WINDOWS_ABSOLUTE = re.compile(r"(? 100: + raise ValueError(f"{name} must not exceed 100") + return value + + def _evidence(value, name, *, validator=None): value = _exact_keys(value, {"status", "value"}, name) status = value["status"] @@ -172,8 +206,16 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - if value.startswith(("http://", "https://", "file:")): - raise ValueError(f"{name} must be a local route") + current = value + for _ in range(4): + if _URI_SCHEME.search(current): + raise ValueError(f"{name} must be a local route") + if _CREDENTIAL_PARAMETER.search(current): + raise ValueError(f"{name} exposes credential-shaped data") + decoded = unquote(current) + if decoded == current: + break + current = decoded return value @@ -260,10 +302,12 @@ def _validate_unresolved(value): raise ValueError("unresolved candidate status is invalid") -def _validate_categories(value): +def _validate_categories(value, *, required=False): value = _mapping(value, "health.categories") if set(value) - set(_CATEGORY_RAW_FIELDS): raise ValueError("health.categories contains an unknown category") + if required and set(value) != set(_CATEGORY_RAW_FIELDS): + raise ValueError("completed health requires every category") for category, fields in _CATEGORY_RAW_FIELDS.items(): if category not in value: continue @@ -283,6 +327,12 @@ def _validate_categories(value): _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + if ( + row["earned"]["status"] == "completed" + and row["available"]["status"] == "completed" + and row["earned"]["value"] > row["available"]["value"] + ): + raise ValueError(f"health.categories.{category}.earned exceeds available") def _collect_unavailable(value, path=""): @@ -355,9 +405,16 @@ def validate_evidence_receipt(value): if health["status"] not in EVIDENCE_STATES: raise ValueError("health.status is invalid") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) - for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health["percentage"], "health.percentage", validator=_percentage) + for field in ("earned_weight", "available_weight"): _evidence(health[field], f"health.{field}", validator=_nonnegative_number) - _validate_categories(health["categories"]) + if ( + health["earned_weight"]["status"] == "completed" + and health["available_weight"]["status"] == "completed" + and health["earned_weight"]["value"] > health["available_weight"]["value"] + ): + raise ValueError("health.earned_weight exceeds available_weight") + _validate_categories(health["categories"], required=health["status"] == "completed") gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") for field in gates: _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) @@ -555,7 +612,7 @@ def build_evidence_receipt( health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { - "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "pages": len(navigation.get("navigated_pages", ())), "hidden_pages": len(navigation.get("hidden_pages", ())), "redirects": len(navigation.get("redirects", ())), } diff --git a/evals/docs-corpus-baseline-v1.json b/evals/docs-corpus-baseline-v1.json index 867c62d..068a6e8 100644 --- a/evals/docs-corpus-baseline-v1.json +++ b/evals/docs-corpus-baseline-v1.json @@ -43,7 +43,7 @@ }, "pages": { "status": "completed", - "value": 110 + "value": 107 }, "redirects": { "status": "completed", diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 8c86091..431ae79 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -12,6 +12,7 @@ import re from collections.abc import Mapping, Sequence from pathlib import Path +from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars from .paths import normalize_repo_relative, safe_path @@ -30,7 +31,21 @@ r"(?:api[_-]?key|authorization|credential|hidden[_-]?reasoning|password|private[_-]?path|raw[_-]?transcript|screenshot|secret|token)", re.IGNORECASE, ) -_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]") +_WINDOWS_ABSOLUTE = re.compile(r"(? 100: + raise ValueError(f"{name} must not exceed 100") + return value + + def _evidence(value, name, *, validator=None): value = _exact_keys(value, {"status", "value"}, name) status = value["status"] @@ -172,8 +206,16 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - if value.startswith(("http://", "https://", "file:")): - raise ValueError(f"{name} must be a local route") + current = value + for _ in range(4): + if _URI_SCHEME.search(current): + raise ValueError(f"{name} must be a local route") + if _CREDENTIAL_PARAMETER.search(current): + raise ValueError(f"{name} exposes credential-shaped data") + decoded = unquote(current) + if decoded == current: + break + current = decoded return value @@ -260,10 +302,12 @@ def _validate_unresolved(value): raise ValueError("unresolved candidate status is invalid") -def _validate_categories(value): +def _validate_categories(value, *, required=False): value = _mapping(value, "health.categories") if set(value) - set(_CATEGORY_RAW_FIELDS): raise ValueError("health.categories contains an unknown category") + if required and set(value) != set(_CATEGORY_RAW_FIELDS): + raise ValueError("completed health requires every category") for category, fields in _CATEGORY_RAW_FIELDS.items(): if category not in value: continue @@ -283,6 +327,12 @@ def _validate_categories(value): _evidence(evidence, f"health.categories.{category}.raw.{field}", validator=validator) _evidence(row["earned"], f"health.categories.{category}.earned", validator=_nonnegative_number) _evidence(row["available"], f"health.categories.{category}.available", validator=_nonnegative_number) + if ( + row["earned"]["status"] == "completed" + and row["available"]["status"] == "completed" + and row["earned"]["value"] > row["available"]["value"] + ): + raise ValueError(f"health.categories.{category}.earned exceeds available") def _collect_unavailable(value, path=""): @@ -355,9 +405,16 @@ def validate_evidence_receipt(value): if health["status"] not in EVIDENCE_STATES: raise ValueError("health.status is invalid") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) - for field in ("percentage", "earned_weight", "available_weight"): + _evidence(health["percentage"], "health.percentage", validator=_percentage) + for field in ("earned_weight", "available_weight"): _evidence(health[field], f"health.{field}", validator=_nonnegative_number) - _validate_categories(health["categories"]) + if ( + health["earned_weight"]["status"] == "completed" + and health["available_weight"]["status"] == "completed" + and health["earned_weight"]["value"] > health["available_weight"]["value"] + ): + raise ValueError("health.earned_weight exceeds available_weight") + _validate_categories(health["categories"], required=health["status"] == "completed") gates = _exact_keys(health["score_gates"], {"map_has_h1", "useful_entry"}, "health.score_gates") for field in gates: _evidence(gates[field], f"health.score_gates.{field}", validator=_boolean) @@ -555,7 +612,7 @@ def build_evidence_receipt( health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { - "pages": len(navigation.get("navigated_pages", ())) + len(navigation.get("hidden_pages", ())), + "pages": len(navigation.get("navigated_pages", ())), "hidden_pages": len(navigation.get("hidden_pages", ())), "redirects": len(navigation.get("redirects", ())), } diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 07a10f9..d449063 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -44,6 +44,20 @@ def _git(root, *args): return completed.stdout.strip() +def _directory_reparse(link, target): + if os.name == "nt": + completed = subprocess.run( + ["cmd", "/c", "mklink", "/J", str(link), str(target)], + capture_output=True, + text=True, + check=False, + ) + if completed.returncode: + raise AssertionError(f"junction creation failed: {completed.stderr.strip()}") + else: + link.symlink_to(target, target_is_directory=True) + + def _fixture_checkout(workspace, spec, files): root = workspace / spec["id"] root.mkdir() @@ -103,6 +117,56 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): with self.assertRaises(ValueError): evidence.validate_evidence_receipt(value) + for target in ( + "/tmp/private/secret.txt", + "/root/.ssh/id_rsa", + r"\\server\private\secret.txt", + "see /tmp/private/secret.txt", + "file:///etc/passwd", + "guide.md?token=SUPERSECRET", + "guide.md?access_token=SUPERSECRET", + "%2Froot%2F.ssh%2Fid_rsa", + "guide.md?%74oken=SUPERSECRET", + ): + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["target"] = { + "status": "completed", + "value": target, + } + with self.subTest(target=target), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["target"] = { + "status": "completed", + "value": "/docs/safe-guide#setup", + } + evidence.validate_evidence_receipt(value) + + value = copy.deepcopy(self.receipt) + value["run"]["model"] = "sk-proj-synthetic012345678901234567890" + with self.assertRaisesRegex(ValueError, "credential-shaped"): + evidence.validate_evidence_receipt(value) + + def test_completed_health_requires_every_category(self): + value = copy.deepcopy(self.receipt) + del value["health"]["categories"]["titles"] + with self.assertRaisesRegex(ValueError, "every category"): + evidence.validate_evidence_receipt(value) + + value = copy.deepcopy(self.receipt) + value["health"]["percentage"] = {"status": "completed", "value": 101} + with self.assertRaisesRegex(ValueError, "exceed 100"): + evidence.validate_evidence_receipt(value) + + value = copy.deepcopy(self.receipt) + value["health"]["categories"]["entry"]["earned"] = { + "status": "completed", + "value": 21, + } + with self.assertRaisesRegex(ValueError, "exceeds available"): + evidence.validate_evidence_receipt(value) + def test_unavailable_is_not_zero_and_must_match_index(self): with self.assertRaises(ValueError): evidence.evidence_value("unavailable", 0) @@ -214,6 +278,8 @@ def test_checked_in_baseline_separates_supported_and_unavailable_evidence(self): evidence.validate_evidence_receipt(row["receipt"]) self.assertTrue(all(item["status"] == "completed" for item in row["configurations"])) self.assertEqual(rows["cline"]["receipt"]["health"]["percentage"]["value"], 29) + self.assertEqual(rows["cline"]["receipt"]["counts"]["pages"]["value"], 107) + self.assertEqual(rows["cline"]["receipt"]["counts"]["hidden_pages"]["value"], 3) for repository_id in set(rows) - {"cline"}: health = rows[repository_id]["receipt"]["health"] self.assertEqual(health["status"], "not_assessed") @@ -285,6 +351,14 @@ def test_checkout_verification_rejects_branch_dirty_and_wrong_commit(self): with self.assertRaisesRegex(ValueError, "not detached"): run_docs_corpus.verify_checkout(workspace, spec) + def test_missing_checkout_error_is_sanitized(self): + with tempfile.TemporaryDirectory() as td: + workspace = Path(td) + spec = copy.deepcopy(run_docs_corpus.load_manifest(CORPUS)["repositories"][0]) + with self.assertRaisesRegex(ValueError, r"^corpus repository is missing: cline$") as raised: + run_docs_corpus.verify_checkout(workspace, spec) + self.assertNotIn(str(workspace), str(raised.exception)) + def test_prepare_refuses_unowned_or_existing_workspace(self): with tempfile.TemporaryDirectory() as td: original = prepare_docs_corpus.WORKSPACE_ROOT @@ -299,6 +373,44 @@ def test_prepare_refuses_unowned_or_existing_workspace(self): finally: prepare_docs_corpus.WORKSPACE_ROOT = original + def test_runner_output_cannot_write_into_workspace_or_through_reparse(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + workspace = root / "workspace" + workspace.mkdir() + target_output = workspace / "cline" / "receipt.json" + with self.assertRaisesRegex(ValueError, "outside the corpus workspace"): + run_docs_corpus._output_path(target_output, workspace) + self.assertFalse(target_output.exists()) + + outside = root / "outside" + outside.mkdir() + alias = root / "alias" + _directory_reparse(alias, outside) + with self.assertRaisesRegex(ValueError, "symlink|reparse"): + run_docs_corpus._output_path(alias / "receipt.json", workspace) + + def test_prepare_reparse_workspace_cannot_write_outside(self): + with tempfile.TemporaryDirectory() as td: + original = prepare_docs_corpus.WORKSPACE_ROOT + base = Path(td) + root = base / "owned" + root.mkdir() + outside = base / "outside" + outside.mkdir() + sentinel = outside / "sentinel.txt" + sentinel.write_text("keep\n", encoding="utf-8") + workspace = root / "docs-corpus-v1" + _directory_reparse(workspace, outside) + try: + prepare_docs_corpus.WORKSPACE_ROOT = root + with self.assertRaisesRegex(ValueError, "symlink|reparse"): + prepare_docs_corpus._workspace(workspace, CORPUS, "docs-corpus-v1") + self.assertEqual(sentinel.read_text(encoding="utf-8"), "keep\n") + self.assertFalse((outside / prepare_docs_corpus.MARKER).exists()) + finally: + prepare_docs_corpus.WORKSPACE_ROOT = original + if __name__ == "__main__": unittest.main() diff --git a/tools/prepare_docs_corpus.py b/tools/prepare_docs_corpus.py index 2181e6e..6e19ace 100644 --- a/tools/prepare_docs_corpus.py +++ b/tools/prepare_docs_corpus.py @@ -14,7 +14,9 @@ DEFAULT_MANIFEST, DEFAULT_WORKSPACE, ROOT, + _assert_no_reparse_components, load_manifest, + safe_path, verify_checkout, ) @@ -40,20 +42,22 @@ def _run(command, *, input_text=None): def _workspace(path, manifest_path, corpus_id): root = WORKSPACE_ROOT.absolute() - workspace = Path(path).absolute() - if Path(root, "sentinel").parent != root: - raise ValueError("workspace root is invalid") + workspace_candidate = Path(path).absolute() try: - workspace.relative_to(root) + workspace_candidate.relative_to(root) except ValueError as exc: raise ValueError("corpus workspace must remain under evals/workspace") from exc - if workspace == root: + if workspace_candidate == root: raise ValueError("corpus workspace must be a named child") + _assert_no_reparse_components(root) root.mkdir(parents=True, exist_ok=True) + _assert_no_reparse_components(root) + workspace = safe_path(workspace_candidate, root) if workspace.exists() and not workspace.is_dir(): raise ValueError("corpus workspace is not a directory") workspace.mkdir(parents=True, exist_ok=True) - marker = workspace / MARKER + workspace = safe_path(workspace, root) + marker = safe_path(workspace / MARKER, workspace) digest = hashlib.sha256(Path(manifest_path).read_bytes()).hexdigest() expected = {"corpus_id": corpus_id, "manifest_sha256": f"sha256:{digest}"} if marker.exists(): @@ -85,7 +89,7 @@ def prepare(manifest=DEFAULT_MANIFEST, workspace=DEFAULT_WORKSPACE, repository_i for spec in manifest_value["repositories"]: if selected and spec["id"] not in selected: continue - target = workspace / spec["id"] + target = safe_path(workspace / spec["id"], workspace) if target.exists(): raise ValueError(f"refusing to update or reuse corpus repository: {spec['id']}") _run(["git", "init", str(target)]) diff --git a/tools/run_docs_corpus.py b/tools/run_docs_corpus.py index a072e2b..0fd7d1a 100644 --- a/tools/run_docs_corpus.py +++ b/tools/run_docs_corpus.py @@ -119,9 +119,9 @@ def _repo_path(workspace, repository_id): _assert_no_reparse_components(workspace) candidate = safe_path(workspace / repository_id, workspace) _assert_no_reparse_components(candidate) - if candidate.is_symlink() or ( - os.name == "nt" - and getattr(candidate.lstat(), "st_file_attributes", 0) & 0x400 + if candidate.exists() and ( + candidate.is_symlink() + or (os.name == "nt" and getattr(candidate.lstat(), "st_file_attributes", 0) & 0x400) ): raise ValueError("corpus repository cannot be a reparse point") return candidate @@ -271,6 +271,19 @@ def run_corpus(manifest=DEFAULT_MANIFEST, workspace=DEFAULT_WORKSPACE): } +def _output_path(value, workspace): + """Reject output paths that could write into a corpus checkout or through a reparse point.""" + workspace = Path(workspace).absolute() + output = Path(value).absolute() + _assert_no_reparse_components(workspace) + _assert_no_reparse_components(output) + try: + output.relative_to(workspace) + except ValueError: + return output + raise ValueError("corpus output must remain outside the corpus workspace") + + def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument("--manifest", default=str(DEFAULT_MANIFEST)) @@ -278,10 +291,12 @@ def main(argv=None): parser.add_argument("--output") namespace = parser.parse_args(argv) try: + output = _output_path(namespace.output, namespace.workspace) if namespace.output else None result = run_corpus(namespace.manifest, namespace.workspace) encoded = json.dumps(result, indent=2, sort_keys=True, ensure_ascii=True) + "\n" - if namespace.output: - Path(namespace.output).write_text(encoded, encoding="utf-8", newline="\n") + if output: + _assert_no_reparse_components(output) + output.write_text(encoded, encoding="utf-8", newline="\n") else: print(encoded, end="") return 0 From 8b7014de36879e8fbb75256e6415bf100301479f Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 02:38:00 -0400 Subject: [PATCH 03/20] Complete 0.1.4 receipt hardening --- .../docs/scripts/_docs_checker/evidence.py | 47 ++++++---- .../copilot/scripts/_docs_checker/evidence.py | 47 ++++++---- .../cursor/scripts/_docs_checker/evidence.py | 47 ++++++---- .../grok/scripts/_docs_checker/evidence.py | 47 ++++++---- .../docs/scripts/_docs_checker/evidence.py | 47 ++++++---- skills/docs/scripts/_docs_checker/evidence.py | 47 ++++++---- tests/test_docs_evidence.py | 89 +++++++++++++++++-- 7 files changed, 252 insertions(+), 119 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 431ae79..2db4cd9 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -15,7 +15,7 @@ from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars -from .paths import normalize_repo_relative, safe_path +from .paths import normalize_repo_relative, safe_path, shared_text_exposes_route EVIDENCE_RECEIPT_VERSION = 1 @@ -37,13 +37,16 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 100: raise ValueError(f"{name} must not exceed 100") return value @@ -198,6 +209,9 @@ def _digest(value, name): def _relative(value, name): + _bounded_text(value, name) + if any(shared_text_exposes_route(current) for current in _decoded_forms(value, name)): + raise ValueError(f"{name} exposes a private or unsafe route") normalized = normalize_repo_relative(value, name) if normalized != value: raise ValueError(f"{name} must be normalized") @@ -206,16 +220,11 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - current = value - for _ in range(4): - if _URI_SCHEME.search(current): + for current in _decoded_forms(value, name): + if _URI_SCHEME.match(current) or _FILE_SCHEME.search(current): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") - decoded = unquote(current) - if decoded == current: - break - current = decoded return value diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 431ae79..2db4cd9 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -15,7 +15,7 @@ from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars -from .paths import normalize_repo_relative, safe_path +from .paths import normalize_repo_relative, safe_path, shared_text_exposes_route EVIDENCE_RECEIPT_VERSION = 1 @@ -37,13 +37,16 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 100: raise ValueError(f"{name} must not exceed 100") return value @@ -198,6 +209,9 @@ def _digest(value, name): def _relative(value, name): + _bounded_text(value, name) + if any(shared_text_exposes_route(current) for current in _decoded_forms(value, name)): + raise ValueError(f"{name} exposes a private or unsafe route") normalized = normalize_repo_relative(value, name) if normalized != value: raise ValueError(f"{name} must be normalized") @@ -206,16 +220,11 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - current = value - for _ in range(4): - if _URI_SCHEME.search(current): + for current in _decoded_forms(value, name): + if _URI_SCHEME.match(current) or _FILE_SCHEME.search(current): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") - decoded = unquote(current) - if decoded == current: - break - current = decoded return value diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 431ae79..2db4cd9 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -15,7 +15,7 @@ from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars -from .paths import normalize_repo_relative, safe_path +from .paths import normalize_repo_relative, safe_path, shared_text_exposes_route EVIDENCE_RECEIPT_VERSION = 1 @@ -37,13 +37,16 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 100: raise ValueError(f"{name} must not exceed 100") return value @@ -198,6 +209,9 @@ def _digest(value, name): def _relative(value, name): + _bounded_text(value, name) + if any(shared_text_exposes_route(current) for current in _decoded_forms(value, name)): + raise ValueError(f"{name} exposes a private or unsafe route") normalized = normalize_repo_relative(value, name) if normalized != value: raise ValueError(f"{name} must be normalized") @@ -206,16 +220,11 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - current = value - for _ in range(4): - if _URI_SCHEME.search(current): + for current in _decoded_forms(value, name): + if _URI_SCHEME.match(current) or _FILE_SCHEME.search(current): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") - decoded = unquote(current) - if decoded == current: - break - current = decoded return value diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 431ae79..2db4cd9 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -15,7 +15,7 @@ from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars -from .paths import normalize_repo_relative, safe_path +from .paths import normalize_repo_relative, safe_path, shared_text_exposes_route EVIDENCE_RECEIPT_VERSION = 1 @@ -37,13 +37,16 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 100: raise ValueError(f"{name} must not exceed 100") return value @@ -198,6 +209,9 @@ def _digest(value, name): def _relative(value, name): + _bounded_text(value, name) + if any(shared_text_exposes_route(current) for current in _decoded_forms(value, name)): + raise ValueError(f"{name} exposes a private or unsafe route") normalized = normalize_repo_relative(value, name) if normalized != value: raise ValueError(f"{name} must be normalized") @@ -206,16 +220,11 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - current = value - for _ in range(4): - if _URI_SCHEME.search(current): + for current in _decoded_forms(value, name): + if _URI_SCHEME.match(current) or _FILE_SCHEME.search(current): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") - decoded = unquote(current) - if decoded == current: - break - current = decoded return value diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 431ae79..2db4cd9 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -15,7 +15,7 @@ from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars -from .paths import normalize_repo_relative, safe_path +from .paths import normalize_repo_relative, safe_path, shared_text_exposes_route EVIDENCE_RECEIPT_VERSION = 1 @@ -37,13 +37,16 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 100: raise ValueError(f"{name} must not exceed 100") return value @@ -198,6 +209,9 @@ def _digest(value, name): def _relative(value, name): + _bounded_text(value, name) + if any(shared_text_exposes_route(current) for current in _decoded_forms(value, name)): + raise ValueError(f"{name} exposes a private or unsafe route") normalized = normalize_repo_relative(value, name) if normalized != value: raise ValueError(f"{name} must be normalized") @@ -206,16 +220,11 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - current = value - for _ in range(4): - if _URI_SCHEME.search(current): + for current in _decoded_forms(value, name): + if _URI_SCHEME.match(current) or _FILE_SCHEME.search(current): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") - decoded = unquote(current) - if decoded == current: - break - current = decoded return value diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 431ae79..2db4cd9 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -15,7 +15,7 @@ from urllib.parse import unquote from .formats import MAX_FRONTMATTER_BYTES, parse_frontmatter_scalars -from .paths import normalize_repo_relative, safe_path +from .paths import normalize_repo_relative, safe_path, shared_text_exposes_route EVIDENCE_RECEIPT_VERSION = 1 @@ -37,13 +37,16 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 100: raise ValueError(f"{name} must not exceed 100") return value @@ -198,6 +209,9 @@ def _digest(value, name): def _relative(value, name): + _bounded_text(value, name) + if any(shared_text_exposes_route(current) for current in _decoded_forms(value, name)): + raise ValueError(f"{name} exposes a private or unsafe route") normalized = normalize_repo_relative(value, name) if normalized != value: raise ValueError(f"{name} must be normalized") @@ -206,16 +220,11 @@ def _relative(value, name): def _route(value, name): value = _bounded_text(value, name) - current = value - for _ in range(4): - if _URI_SCHEME.search(current): + for current in _decoded_forms(value, name): + if _URI_SCHEME.match(current) or _FILE_SCHEME.search(current): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") - decoded = unquote(current) - if decoded == current: - break - current = decoded return value diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index d449063..dd5be51 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -6,6 +6,7 @@ import tempfile import unittest from pathlib import Path +from urllib.parse import quote ROOT = Path(__file__).parents[1] @@ -127,6 +128,7 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): "guide.md?access_token=SUPERSECRET", "%2Froot%2F.ssh%2Fid_rsa", "guide.md?%74oken=SUPERSECRET", + "#api:v1?token=SUPERSECRET", ): value = copy.deepcopy(self.receipt) value["evidence"]["deterministic"]["findings"][0]["target"] = { @@ -139,14 +141,51 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): value = copy.deepcopy(self.receipt) value["evidence"]["deterministic"]["findings"][0]["target"] = { "status": "completed", - "value": "/docs/safe-guide#setup", + "value": "/docs/safe-guide#api:v1", } evidence.validate_evidence_receipt(value) - value = copy.deepcopy(self.receipt) - value["run"]["model"] = "sk-proj-synthetic012345678901234567890" - with self.assertRaisesRegex(ValueError, "credential-shaped"): - evidence.validate_evidence_receipt(value) + for credential in ( + "sk-proj-synthetic012345678901234567890", + "glpat-synthetic012345678901234567890", + "npm_012345678901234567890123456789012345", + "pypi-synthetic012345678901234567890", + "eyJsynthetic01.eyJsynthetic02.synthetic-signature", + ): + value = copy.deepcopy(self.receipt) + value["run"]["model"] = credential + with self.subTest(credential=credential), self.assertRaisesRegex( + ValueError, "credential-shaped" + ): + evidence.validate_evidence_receipt(value) + + for private_path in ( + ".local/private.md", + r"\Users\private\notes.md", + "docs/guide.md?token=SUPERSECRET", + quote(quote(quote(quote("/root/.ssh/id_rsa", safe=""), safe=""), safe=""), safe=""), + ): + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["path"] = { + "status": "completed", + "value": private_path, + } + with self.subTest(private_path=private_path), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + + deeply_encoded_target = "/tmp/private/secret.txt" + deeply_encoded_parameter = "guide.md?token=SUPERSECRET" + for _ in range(4): + deeply_encoded_target = quote(deeply_encoded_target, safe="") + deeply_encoded_parameter = quote(deeply_encoded_parameter, safe="") + for target in (deeply_encoded_target, deeply_encoded_parameter): + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["target"] = { + "status": "completed", + "value": target, + } + with self.subTest(target=target), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) def test_completed_health_requires_every_category(self): value = copy.deepcopy(self.receipt) @@ -159,6 +198,11 @@ def test_completed_health_requires_every_category(self): with self.assertRaisesRegex(ValueError, "exceed 100"): evidence.validate_evidence_receipt(value) + value = copy.deepcopy(self.receipt) + value["health"]["percentage"] = {"status": "completed", "value": 29.5} + with self.assertRaisesRegex(ValueError, "integer"): + evidence.validate_evidence_receipt(value) + value = copy.deepcopy(self.receipt) value["health"]["categories"]["entry"]["earned"] = { "status": "completed", @@ -167,6 +211,41 @@ def test_completed_health_requires_every_category(self): with self.assertRaisesRegex(ValueError, "exceeds available"): evidence.validate_evidence_receipt(value) + def test_builder_counts_hidden_pages_separately_from_pages(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + docs = root / "docs" + docs.mkdir() + (docs / "README.md").write_text("# Home\n\n## Start\n", encoding="utf-8") + findings, _, measurements = run_docs_corpus.check( + root, + map_path="docs/README.md", + scope="docs", + _measurements=True, + ) + health = run_docs_corpus.health_summary( + measurements, + findings=measurements["active_findings"], + baseline=measurements["baseline"], + freshness=measurements["freshness"], + coverage=measurements["coverage"], + ) + navigation = copy.deepcopy(measurements["navigation"]) + navigation["navigated_pages"] = ["docs/README.md", "docs/guide.md"] + navigation["hidden_pages"] = ["docs/guide.md"] + receipt = evidence.build_evidence_receipt( + receipt_id="page-count-regression", + repository_identifier="example.invalid/docs/page-count", + commit="0" * 40, + checker_version="0.1.4", + run=copy.deepcopy(self.receipt["run"]), + checker_payload={"navigation": navigation, "health": health, "findings": findings}, + orientation=copy.deepcopy(self.receipt["orientation"]), + semantic=run_docs_corpus._semantic_not_assessed(), + ) + self.assertEqual(receipt["counts"]["pages"], {"status": "completed", "value": 2}) + self.assertEqual(receipt["counts"]["hidden_pages"], {"status": "completed", "value": 1}) + def test_unavailable_is_not_zero_and_must_match_index(self): with self.assertRaises(ValueError): evidence.evidence_value("unavailable", 0) From 6a27eb669fb936b31a36746585bbb4f03af69cb3 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 03:14:44 -0400 Subject: [PATCH 04/20] Close 0.1.4 evidence review gaps --- .../docs/scripts/_docs_checker/evidence.py | 131 ++++--- .../skills/docs/scripts/evidence_receipt.py | 21 +- .../copilot/scripts/_docs_checker/evidence.py | 131 ++++--- adapters/copilot/scripts/evidence_receipt.py | 21 +- .../cursor/scripts/_docs_checker/evidence.py | 131 ++++--- adapters/cursor/scripts/evidence_receipt.py | 21 +- .../grok/scripts/_docs_checker/evidence.py | 131 ++++--- adapters/grok/scripts/evidence_receipt.py | 21 +- .../docs/scripts/_docs_checker/evidence.py | 131 ++++--- .../skills/docs/scripts/evidence_receipt.py | 21 +- skills/docs/scripts/_docs_checker/evidence.py | 131 ++++--- skills/docs/scripts/evidence_receipt.py | 21 +- tests/test_docs_evidence.py | 359 +++++++++++++++++- tools/prepare_docs_corpus.py | 52 ++- tools/run_docs_corpus.py | 49 ++- 15 files changed, 1030 insertions(+), 342 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 2db4cd9..e3844f7 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -23,6 +23,7 @@ MAX_RECEIPT_BYTES = 512 * 1024 MAX_TEXT_BYTES = 512 MAX_FINDINGS = 10_000 +_OMITTED = object() _SHA = re.compile(r"^[0-9a-f]{40}$") _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") @@ -37,8 +38,7 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 1_000: raise ValueError("evidence.unresolved.candidates exceeds capacity") @@ -307,8 +321,21 @@ def _validate_unresolved(value): candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" ) _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") - if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: - raise ValueError("unresolved candidate status is invalid") + _enum_text( + candidate["status"], + {"not_assessed", "unavailable", "failed"}, + "unresolved candidate status", + ) + + +def _validate_doctor(value): + value = _exact_keys( + value, {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + _enum_text(value["status"], EVIDENCE_STATES, "doctor.status") + _evidence(value["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(value["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + return value def _validate_categories(value, *, required=False): @@ -411,8 +438,7 @@ def validate_evidence_receipt(value): {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, "health", ) - if health["status"] not in EVIDENCE_STATES: - raise ValueError("health.status is invalid") + _enum_text(health["status"], EVIDENCE_STATES, "health.status") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) _evidence(health["percentage"], "health.percentage", validator=_percentage) for field in ("earned_weight", "available_weight"): @@ -435,19 +461,12 @@ def validate_evidence_receipt(value): _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) _validate_unresolved(evidence["unresolved"]) - doctor = _exact_keys( - value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" - ) - if doctor["status"] not in EVIDENCE_STATES: - raise ValueError("doctor.status is invalid") - _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) - _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + _validate_doctor(value["doctor"]) write_audit = _exact_keys( value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" ) - if write_audit["status"] not in EVIDENCE_STATES: - raise ValueError("write_audit.status is invalid") + _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) @@ -609,7 +628,7 @@ def build_evidence_receipt( orientation, semantic, unresolved=(), - doctor=None, + doctor=_OMITTED, writes_attempted=0, writes_observed=0, git_before="clean", @@ -617,7 +636,33 @@ def build_evidence_receipt( ): """Build one receipt from existing deterministic checker evidence.""" checker_payload = _mapping(checker_payload, "checker payload") - navigation = checker_payload.get("navigation", {}) + navigation = _mapping(checker_payload.get("navigation", {}), "checker payload.navigation") + _validate_run(run) + run = dict(run) + orientation = _exact_keys( + orientation, + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + semantic = _exact_keys(semantic, {"status", "evaluator", "findings"}, "semantic") + _validate_lane(semantic, "semantic", semantic=True) + unresolved = list(_sequence(unresolved, "unresolved")) + _validate_unresolved({"status": "completed", "candidates": unresolved}) + if doctor is _OMITTED: + doctor = { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + else: + doctor = dict(_validate_doctor(doctor)) health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { @@ -650,20 +695,10 @@ def build_evidence_receipt( target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) - semantic = _mapping(semantic, "semantic") - evaluator = semantic.get("evaluator", {}) semantic_lane = { "status": semantic["status"], - "evaluator": { - field: evaluator.get(field, evidence_value("not_assessed")) - for field in ("provider", "model", "version") - }, - "findings": list(semantic.get("findings", ())), - } - doctor = doctor or { - "status": "not_assessed", - "treatment_fingerprint": evidence_value("not_assessed"), - "approval_line_present": evidence_value("not_assessed"), + "evaluator": dict(semantic["evaluator"]), + "findings": list(semantic["findings"]), } receipt = { "schema_version": EVIDENCE_RECEIPT_VERSION, @@ -675,7 +710,7 @@ def build_evidence_receipt( else evidence_value("unavailable"), }, "checker": {"name": "diataxis-docs", "version": checker_version}, - "run": dict(run), + "run": run, "surface": { "provider": navigation.get("provider", "unknown"), "authority": evidence_value("completed", navigation["authority"]) diff --git a/adapters/claude/skills/docs/scripts/evidence_receipt.py b/adapters/claude/skills/docs/scripts/evidence_receipt.py index ce0fbb9..b79532a 100644 --- a/adapters/claude/skills/docs/scripts/evidence_receipt.py +++ b/adapters/claude/skills/docs/scripts/evidence_receipt.py @@ -10,6 +10,7 @@ from pathlib import Path from _docs_checker.evidence import ( + MAX_RECEIPT_BYTES, build_evidence_receipt, canonical_receipt_bytes, observe_entry_orientation, @@ -46,8 +47,15 @@ def _status(root): def _metadata(path): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_RECEIPT_BYTES + 1) + except OSError as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + if len(raw) > MAX_RECEIPT_BYTES: + raise ValueError("metadata file exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("metadata file is unavailable or malformed") from exc expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} if not isinstance(value, dict) or set(value) != expected: @@ -103,7 +111,14 @@ def main(argv=None): ) sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError, RecursionError): + print( + json.dumps( + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None} + ) + ) + return 2 + except ValueError as exc: print( json.dumps( {"status": "failed", "error": str(exc), "receipt": None}, diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 2db4cd9..e3844f7 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -23,6 +23,7 @@ MAX_RECEIPT_BYTES = 512 * 1024 MAX_TEXT_BYTES = 512 MAX_FINDINGS = 10_000 +_OMITTED = object() _SHA = re.compile(r"^[0-9a-f]{40}$") _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") @@ -37,8 +38,7 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 1_000: raise ValueError("evidence.unresolved.candidates exceeds capacity") @@ -307,8 +321,21 @@ def _validate_unresolved(value): candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" ) _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") - if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: - raise ValueError("unresolved candidate status is invalid") + _enum_text( + candidate["status"], + {"not_assessed", "unavailable", "failed"}, + "unresolved candidate status", + ) + + +def _validate_doctor(value): + value = _exact_keys( + value, {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + _enum_text(value["status"], EVIDENCE_STATES, "doctor.status") + _evidence(value["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(value["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + return value def _validate_categories(value, *, required=False): @@ -411,8 +438,7 @@ def validate_evidence_receipt(value): {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, "health", ) - if health["status"] not in EVIDENCE_STATES: - raise ValueError("health.status is invalid") + _enum_text(health["status"], EVIDENCE_STATES, "health.status") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) _evidence(health["percentage"], "health.percentage", validator=_percentage) for field in ("earned_weight", "available_weight"): @@ -435,19 +461,12 @@ def validate_evidence_receipt(value): _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) _validate_unresolved(evidence["unresolved"]) - doctor = _exact_keys( - value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" - ) - if doctor["status"] not in EVIDENCE_STATES: - raise ValueError("doctor.status is invalid") - _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) - _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + _validate_doctor(value["doctor"]) write_audit = _exact_keys( value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" ) - if write_audit["status"] not in EVIDENCE_STATES: - raise ValueError("write_audit.status is invalid") + _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) @@ -609,7 +628,7 @@ def build_evidence_receipt( orientation, semantic, unresolved=(), - doctor=None, + doctor=_OMITTED, writes_attempted=0, writes_observed=0, git_before="clean", @@ -617,7 +636,33 @@ def build_evidence_receipt( ): """Build one receipt from existing deterministic checker evidence.""" checker_payload = _mapping(checker_payload, "checker payload") - navigation = checker_payload.get("navigation", {}) + navigation = _mapping(checker_payload.get("navigation", {}), "checker payload.navigation") + _validate_run(run) + run = dict(run) + orientation = _exact_keys( + orientation, + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + semantic = _exact_keys(semantic, {"status", "evaluator", "findings"}, "semantic") + _validate_lane(semantic, "semantic", semantic=True) + unresolved = list(_sequence(unresolved, "unresolved")) + _validate_unresolved({"status": "completed", "candidates": unresolved}) + if doctor is _OMITTED: + doctor = { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + else: + doctor = dict(_validate_doctor(doctor)) health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { @@ -650,20 +695,10 @@ def build_evidence_receipt( target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) - semantic = _mapping(semantic, "semantic") - evaluator = semantic.get("evaluator", {}) semantic_lane = { "status": semantic["status"], - "evaluator": { - field: evaluator.get(field, evidence_value("not_assessed")) - for field in ("provider", "model", "version") - }, - "findings": list(semantic.get("findings", ())), - } - doctor = doctor or { - "status": "not_assessed", - "treatment_fingerprint": evidence_value("not_assessed"), - "approval_line_present": evidence_value("not_assessed"), + "evaluator": dict(semantic["evaluator"]), + "findings": list(semantic["findings"]), } receipt = { "schema_version": EVIDENCE_RECEIPT_VERSION, @@ -675,7 +710,7 @@ def build_evidence_receipt( else evidence_value("unavailable"), }, "checker": {"name": "diataxis-docs", "version": checker_version}, - "run": dict(run), + "run": run, "surface": { "provider": navigation.get("provider", "unknown"), "authority": evidence_value("completed", navigation["authority"]) diff --git a/adapters/copilot/scripts/evidence_receipt.py b/adapters/copilot/scripts/evidence_receipt.py index ce0fbb9..b79532a 100644 --- a/adapters/copilot/scripts/evidence_receipt.py +++ b/adapters/copilot/scripts/evidence_receipt.py @@ -10,6 +10,7 @@ from pathlib import Path from _docs_checker.evidence import ( + MAX_RECEIPT_BYTES, build_evidence_receipt, canonical_receipt_bytes, observe_entry_orientation, @@ -46,8 +47,15 @@ def _status(root): def _metadata(path): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_RECEIPT_BYTES + 1) + except OSError as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + if len(raw) > MAX_RECEIPT_BYTES: + raise ValueError("metadata file exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("metadata file is unavailable or malformed") from exc expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} if not isinstance(value, dict) or set(value) != expected: @@ -103,7 +111,14 @@ def main(argv=None): ) sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError, RecursionError): + print( + json.dumps( + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None} + ) + ) + return 2 + except ValueError as exc: print( json.dumps( {"status": "failed", "error": str(exc), "receipt": None}, diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 2db4cd9..e3844f7 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -23,6 +23,7 @@ MAX_RECEIPT_BYTES = 512 * 1024 MAX_TEXT_BYTES = 512 MAX_FINDINGS = 10_000 +_OMITTED = object() _SHA = re.compile(r"^[0-9a-f]{40}$") _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") @@ -37,8 +38,7 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 1_000: raise ValueError("evidence.unresolved.candidates exceeds capacity") @@ -307,8 +321,21 @@ def _validate_unresolved(value): candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" ) _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") - if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: - raise ValueError("unresolved candidate status is invalid") + _enum_text( + candidate["status"], + {"not_assessed", "unavailable", "failed"}, + "unresolved candidate status", + ) + + +def _validate_doctor(value): + value = _exact_keys( + value, {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + _enum_text(value["status"], EVIDENCE_STATES, "doctor.status") + _evidence(value["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(value["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + return value def _validate_categories(value, *, required=False): @@ -411,8 +438,7 @@ def validate_evidence_receipt(value): {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, "health", ) - if health["status"] not in EVIDENCE_STATES: - raise ValueError("health.status is invalid") + _enum_text(health["status"], EVIDENCE_STATES, "health.status") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) _evidence(health["percentage"], "health.percentage", validator=_percentage) for field in ("earned_weight", "available_weight"): @@ -435,19 +461,12 @@ def validate_evidence_receipt(value): _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) _validate_unresolved(evidence["unresolved"]) - doctor = _exact_keys( - value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" - ) - if doctor["status"] not in EVIDENCE_STATES: - raise ValueError("doctor.status is invalid") - _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) - _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + _validate_doctor(value["doctor"]) write_audit = _exact_keys( value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" ) - if write_audit["status"] not in EVIDENCE_STATES: - raise ValueError("write_audit.status is invalid") + _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) @@ -609,7 +628,7 @@ def build_evidence_receipt( orientation, semantic, unresolved=(), - doctor=None, + doctor=_OMITTED, writes_attempted=0, writes_observed=0, git_before="clean", @@ -617,7 +636,33 @@ def build_evidence_receipt( ): """Build one receipt from existing deterministic checker evidence.""" checker_payload = _mapping(checker_payload, "checker payload") - navigation = checker_payload.get("navigation", {}) + navigation = _mapping(checker_payload.get("navigation", {}), "checker payload.navigation") + _validate_run(run) + run = dict(run) + orientation = _exact_keys( + orientation, + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + semantic = _exact_keys(semantic, {"status", "evaluator", "findings"}, "semantic") + _validate_lane(semantic, "semantic", semantic=True) + unresolved = list(_sequence(unresolved, "unresolved")) + _validate_unresolved({"status": "completed", "candidates": unresolved}) + if doctor is _OMITTED: + doctor = { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + else: + doctor = dict(_validate_doctor(doctor)) health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { @@ -650,20 +695,10 @@ def build_evidence_receipt( target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) - semantic = _mapping(semantic, "semantic") - evaluator = semantic.get("evaluator", {}) semantic_lane = { "status": semantic["status"], - "evaluator": { - field: evaluator.get(field, evidence_value("not_assessed")) - for field in ("provider", "model", "version") - }, - "findings": list(semantic.get("findings", ())), - } - doctor = doctor or { - "status": "not_assessed", - "treatment_fingerprint": evidence_value("not_assessed"), - "approval_line_present": evidence_value("not_assessed"), + "evaluator": dict(semantic["evaluator"]), + "findings": list(semantic["findings"]), } receipt = { "schema_version": EVIDENCE_RECEIPT_VERSION, @@ -675,7 +710,7 @@ def build_evidence_receipt( else evidence_value("unavailable"), }, "checker": {"name": "diataxis-docs", "version": checker_version}, - "run": dict(run), + "run": run, "surface": { "provider": navigation.get("provider", "unknown"), "authority": evidence_value("completed", navigation["authority"]) diff --git a/adapters/cursor/scripts/evidence_receipt.py b/adapters/cursor/scripts/evidence_receipt.py index ce0fbb9..b79532a 100644 --- a/adapters/cursor/scripts/evidence_receipt.py +++ b/adapters/cursor/scripts/evidence_receipt.py @@ -10,6 +10,7 @@ from pathlib import Path from _docs_checker.evidence import ( + MAX_RECEIPT_BYTES, build_evidence_receipt, canonical_receipt_bytes, observe_entry_orientation, @@ -46,8 +47,15 @@ def _status(root): def _metadata(path): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_RECEIPT_BYTES + 1) + except OSError as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + if len(raw) > MAX_RECEIPT_BYTES: + raise ValueError("metadata file exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("metadata file is unavailable or malformed") from exc expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} if not isinstance(value, dict) or set(value) != expected: @@ -103,7 +111,14 @@ def main(argv=None): ) sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError, RecursionError): + print( + json.dumps( + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None} + ) + ) + return 2 + except ValueError as exc: print( json.dumps( {"status": "failed", "error": str(exc), "receipt": None}, diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 2db4cd9..e3844f7 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -23,6 +23,7 @@ MAX_RECEIPT_BYTES = 512 * 1024 MAX_TEXT_BYTES = 512 MAX_FINDINGS = 10_000 +_OMITTED = object() _SHA = re.compile(r"^[0-9a-f]{40}$") _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") @@ -37,8 +38,7 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 1_000: raise ValueError("evidence.unresolved.candidates exceeds capacity") @@ -307,8 +321,21 @@ def _validate_unresolved(value): candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" ) _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") - if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: - raise ValueError("unresolved candidate status is invalid") + _enum_text( + candidate["status"], + {"not_assessed", "unavailable", "failed"}, + "unresolved candidate status", + ) + + +def _validate_doctor(value): + value = _exact_keys( + value, {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + _enum_text(value["status"], EVIDENCE_STATES, "doctor.status") + _evidence(value["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(value["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + return value def _validate_categories(value, *, required=False): @@ -411,8 +438,7 @@ def validate_evidence_receipt(value): {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, "health", ) - if health["status"] not in EVIDENCE_STATES: - raise ValueError("health.status is invalid") + _enum_text(health["status"], EVIDENCE_STATES, "health.status") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) _evidence(health["percentage"], "health.percentage", validator=_percentage) for field in ("earned_weight", "available_weight"): @@ -435,19 +461,12 @@ def validate_evidence_receipt(value): _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) _validate_unresolved(evidence["unresolved"]) - doctor = _exact_keys( - value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" - ) - if doctor["status"] not in EVIDENCE_STATES: - raise ValueError("doctor.status is invalid") - _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) - _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + _validate_doctor(value["doctor"]) write_audit = _exact_keys( value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" ) - if write_audit["status"] not in EVIDENCE_STATES: - raise ValueError("write_audit.status is invalid") + _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) @@ -609,7 +628,7 @@ def build_evidence_receipt( orientation, semantic, unresolved=(), - doctor=None, + doctor=_OMITTED, writes_attempted=0, writes_observed=0, git_before="clean", @@ -617,7 +636,33 @@ def build_evidence_receipt( ): """Build one receipt from existing deterministic checker evidence.""" checker_payload = _mapping(checker_payload, "checker payload") - navigation = checker_payload.get("navigation", {}) + navigation = _mapping(checker_payload.get("navigation", {}), "checker payload.navigation") + _validate_run(run) + run = dict(run) + orientation = _exact_keys( + orientation, + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + semantic = _exact_keys(semantic, {"status", "evaluator", "findings"}, "semantic") + _validate_lane(semantic, "semantic", semantic=True) + unresolved = list(_sequence(unresolved, "unresolved")) + _validate_unresolved({"status": "completed", "candidates": unresolved}) + if doctor is _OMITTED: + doctor = { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + else: + doctor = dict(_validate_doctor(doctor)) health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { @@ -650,20 +695,10 @@ def build_evidence_receipt( target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) - semantic = _mapping(semantic, "semantic") - evaluator = semantic.get("evaluator", {}) semantic_lane = { "status": semantic["status"], - "evaluator": { - field: evaluator.get(field, evidence_value("not_assessed")) - for field in ("provider", "model", "version") - }, - "findings": list(semantic.get("findings", ())), - } - doctor = doctor or { - "status": "not_assessed", - "treatment_fingerprint": evidence_value("not_assessed"), - "approval_line_present": evidence_value("not_assessed"), + "evaluator": dict(semantic["evaluator"]), + "findings": list(semantic["findings"]), } receipt = { "schema_version": EVIDENCE_RECEIPT_VERSION, @@ -675,7 +710,7 @@ def build_evidence_receipt( else evidence_value("unavailable"), }, "checker": {"name": "diataxis-docs", "version": checker_version}, - "run": dict(run), + "run": run, "surface": { "provider": navigation.get("provider", "unknown"), "authority": evidence_value("completed", navigation["authority"]) diff --git a/adapters/grok/scripts/evidence_receipt.py b/adapters/grok/scripts/evidence_receipt.py index ce0fbb9..b79532a 100644 --- a/adapters/grok/scripts/evidence_receipt.py +++ b/adapters/grok/scripts/evidence_receipt.py @@ -10,6 +10,7 @@ from pathlib import Path from _docs_checker.evidence import ( + MAX_RECEIPT_BYTES, build_evidence_receipt, canonical_receipt_bytes, observe_entry_orientation, @@ -46,8 +47,15 @@ def _status(root): def _metadata(path): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_RECEIPT_BYTES + 1) + except OSError as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + if len(raw) > MAX_RECEIPT_BYTES: + raise ValueError("metadata file exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("metadata file is unavailable or malformed") from exc expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} if not isinstance(value, dict) or set(value) != expected: @@ -103,7 +111,14 @@ def main(argv=None): ) sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError, RecursionError): + print( + json.dumps( + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None} + ) + ) + return 2 + except ValueError as exc: print( json.dumps( {"status": "failed", "error": str(exc), "receipt": None}, diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 2db4cd9..e3844f7 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -23,6 +23,7 @@ MAX_RECEIPT_BYTES = 512 * 1024 MAX_TEXT_BYTES = 512 MAX_FINDINGS = 10_000 +_OMITTED = object() _SHA = re.compile(r"^[0-9a-f]{40}$") _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") @@ -37,8 +38,7 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 1_000: raise ValueError("evidence.unresolved.candidates exceeds capacity") @@ -307,8 +321,21 @@ def _validate_unresolved(value): candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" ) _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") - if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: - raise ValueError("unresolved candidate status is invalid") + _enum_text( + candidate["status"], + {"not_assessed", "unavailable", "failed"}, + "unresolved candidate status", + ) + + +def _validate_doctor(value): + value = _exact_keys( + value, {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + _enum_text(value["status"], EVIDENCE_STATES, "doctor.status") + _evidence(value["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(value["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + return value def _validate_categories(value, *, required=False): @@ -411,8 +438,7 @@ def validate_evidence_receipt(value): {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, "health", ) - if health["status"] not in EVIDENCE_STATES: - raise ValueError("health.status is invalid") + _enum_text(health["status"], EVIDENCE_STATES, "health.status") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) _evidence(health["percentage"], "health.percentage", validator=_percentage) for field in ("earned_weight", "available_weight"): @@ -435,19 +461,12 @@ def validate_evidence_receipt(value): _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) _validate_unresolved(evidence["unresolved"]) - doctor = _exact_keys( - value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" - ) - if doctor["status"] not in EVIDENCE_STATES: - raise ValueError("doctor.status is invalid") - _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) - _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + _validate_doctor(value["doctor"]) write_audit = _exact_keys( value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" ) - if write_audit["status"] not in EVIDENCE_STATES: - raise ValueError("write_audit.status is invalid") + _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) @@ -609,7 +628,7 @@ def build_evidence_receipt( orientation, semantic, unresolved=(), - doctor=None, + doctor=_OMITTED, writes_attempted=0, writes_observed=0, git_before="clean", @@ -617,7 +636,33 @@ def build_evidence_receipt( ): """Build one receipt from existing deterministic checker evidence.""" checker_payload = _mapping(checker_payload, "checker payload") - navigation = checker_payload.get("navigation", {}) + navigation = _mapping(checker_payload.get("navigation", {}), "checker payload.navigation") + _validate_run(run) + run = dict(run) + orientation = _exact_keys( + orientation, + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + semantic = _exact_keys(semantic, {"status", "evaluator", "findings"}, "semantic") + _validate_lane(semantic, "semantic", semantic=True) + unresolved = list(_sequence(unresolved, "unresolved")) + _validate_unresolved({"status": "completed", "candidates": unresolved}) + if doctor is _OMITTED: + doctor = { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + else: + doctor = dict(_validate_doctor(doctor)) health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { @@ -650,20 +695,10 @@ def build_evidence_receipt( target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) - semantic = _mapping(semantic, "semantic") - evaluator = semantic.get("evaluator", {}) semantic_lane = { "status": semantic["status"], - "evaluator": { - field: evaluator.get(field, evidence_value("not_assessed")) - for field in ("provider", "model", "version") - }, - "findings": list(semantic.get("findings", ())), - } - doctor = doctor or { - "status": "not_assessed", - "treatment_fingerprint": evidence_value("not_assessed"), - "approval_line_present": evidence_value("not_assessed"), + "evaluator": dict(semantic["evaluator"]), + "findings": list(semantic["findings"]), } receipt = { "schema_version": EVIDENCE_RECEIPT_VERSION, @@ -675,7 +710,7 @@ def build_evidence_receipt( else evidence_value("unavailable"), }, "checker": {"name": "diataxis-docs", "version": checker_version}, - "run": dict(run), + "run": run, "surface": { "provider": navigation.get("provider", "unknown"), "authority": evidence_value("completed", navigation["authority"]) diff --git a/adapters/plugin/skills/docs/scripts/evidence_receipt.py b/adapters/plugin/skills/docs/scripts/evidence_receipt.py index ce0fbb9..b79532a 100644 --- a/adapters/plugin/skills/docs/scripts/evidence_receipt.py +++ b/adapters/plugin/skills/docs/scripts/evidence_receipt.py @@ -10,6 +10,7 @@ from pathlib import Path from _docs_checker.evidence import ( + MAX_RECEIPT_BYTES, build_evidence_receipt, canonical_receipt_bytes, observe_entry_orientation, @@ -46,8 +47,15 @@ def _status(root): def _metadata(path): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_RECEIPT_BYTES + 1) + except OSError as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + if len(raw) > MAX_RECEIPT_BYTES: + raise ValueError("metadata file exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("metadata file is unavailable or malformed") from exc expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} if not isinstance(value, dict) or set(value) != expected: @@ -103,7 +111,14 @@ def main(argv=None): ) sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError, RecursionError): + print( + json.dumps( + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None} + ) + ) + return 2 + except ValueError as exc: print( json.dumps( {"status": "failed", "error": str(exc), "receipt": None}, diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 2db4cd9..e3844f7 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -23,6 +23,7 @@ MAX_RECEIPT_BYTES = 512 * 1024 MAX_TEXT_BYTES = 512 MAX_FINDINGS = 10_000 +_OMITTED = object() _SHA = re.compile(r"^[0-9a-f]{40}$") _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") @@ -37,8 +38,7 @@ re.IGNORECASE, ) _NETWORK_PATH = re.compile(r"(? 1_000: raise ValueError("evidence.unresolved.candidates exceeds capacity") @@ -307,8 +321,21 @@ def _validate_unresolved(value): candidate, {"kind", "status"}, f"evidence.unresolved.candidates[{index}]" ) _safe_identifier(candidate["kind"], f"evidence.unresolved.candidates[{index}].kind") - if candidate["status"] not in {"not_assessed", "unavailable", "failed"}: - raise ValueError("unresolved candidate status is invalid") + _enum_text( + candidate["status"], + {"not_assessed", "unavailable", "failed"}, + "unresolved candidate status", + ) + + +def _validate_doctor(value): + value = _exact_keys( + value, {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" + ) + _enum_text(value["status"], EVIDENCE_STATES, "doctor.status") + _evidence(value["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) + _evidence(value["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + return value def _validate_categories(value, *, required=False): @@ -411,8 +438,7 @@ def validate_evidence_receipt(value): {"status", "rubric_version", "percentage", "earned_weight", "available_weight", "categories", "score_gates"}, "health", ) - if health["status"] not in EVIDENCE_STATES: - raise ValueError("health.status is invalid") + _enum_text(health["status"], EVIDENCE_STATES, "health.status") _evidence(health["rubric_version"], "health.rubric_version", validator=_integer) _evidence(health["percentage"], "health.percentage", validator=_percentage) for field in ("earned_weight", "available_weight"): @@ -435,19 +461,12 @@ def validate_evidence_receipt(value): _validate_lane(evidence["semantic"], "evidence.semantic", semantic=True) _validate_unresolved(evidence["unresolved"]) - doctor = _exact_keys( - value["doctor"], {"status", "treatment_fingerprint", "approval_line_present"}, "doctor" - ) - if doctor["status"] not in EVIDENCE_STATES: - raise ValueError("doctor.status is invalid") - _evidence(doctor["treatment_fingerprint"], "doctor.treatment_fingerprint", validator=_digest) - _evidence(doctor["approval_line_present"], "doctor.approval_line_present", validator=_boolean) + _validate_doctor(value["doctor"]) write_audit = _exact_keys( value["write_audit"], {"status", "writes_attempted", "writes_observed"}, "write_audit" ) - if write_audit["status"] not in EVIDENCE_STATES: - raise ValueError("write_audit.status is invalid") + _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) @@ -609,7 +628,7 @@ def build_evidence_receipt( orientation, semantic, unresolved=(), - doctor=None, + doctor=_OMITTED, writes_attempted=0, writes_observed=0, git_before="clean", @@ -617,7 +636,33 @@ def build_evidence_receipt( ): """Build one receipt from existing deterministic checker evidence.""" checker_payload = _mapping(checker_payload, "checker payload") - navigation = checker_payload.get("navigation", {}) + navigation = _mapping(checker_payload.get("navigation", {}), "checker payload.navigation") + _validate_run(run) + run = dict(run) + orientation = _exact_keys( + orientation, + {"literal_h1", "frontmatter_title", "provider_rendered_title"}, + "orientation", + ) + _evidence(orientation["literal_h1"], "orientation.literal_h1", validator=_boolean) + _evidence(orientation["frontmatter_title"], "orientation.frontmatter_title", validator=_boolean) + _evidence( + orientation["provider_rendered_title"], + "orientation.provider_rendered_title", + validator=_boolean, + ) + semantic = _exact_keys(semantic, {"status", "evaluator", "findings"}, "semantic") + _validate_lane(semantic, "semantic", semantic=True) + unresolved = list(_sequence(unresolved, "unresolved")) + _validate_unresolved({"status": "completed", "candidates": unresolved}) + if doctor is _OMITTED: + doctor = { + "status": "not_assessed", + "treatment_fingerprint": evidence_value("not_assessed"), + "approval_line_present": evidence_value("not_assessed"), + } + else: + doctor = dict(_validate_doctor(doctor)) health = health_receipt(checker_payload.get("health")) measured = isinstance(checker_payload.get("health"), Mapping) counts = { @@ -650,20 +695,10 @@ def build_evidence_receipt( target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) - semantic = _mapping(semantic, "semantic") - evaluator = semantic.get("evaluator", {}) semantic_lane = { "status": semantic["status"], - "evaluator": { - field: evaluator.get(field, evidence_value("not_assessed")) - for field in ("provider", "model", "version") - }, - "findings": list(semantic.get("findings", ())), - } - doctor = doctor or { - "status": "not_assessed", - "treatment_fingerprint": evidence_value("not_assessed"), - "approval_line_present": evidence_value("not_assessed"), + "evaluator": dict(semantic["evaluator"]), + "findings": list(semantic["findings"]), } receipt = { "schema_version": EVIDENCE_RECEIPT_VERSION, @@ -675,7 +710,7 @@ def build_evidence_receipt( else evidence_value("unavailable"), }, "checker": {"name": "diataxis-docs", "version": checker_version}, - "run": dict(run), + "run": run, "surface": { "provider": navigation.get("provider", "unknown"), "authority": evidence_value("completed", navigation["authority"]) diff --git a/skills/docs/scripts/evidence_receipt.py b/skills/docs/scripts/evidence_receipt.py index ce0fbb9..b79532a 100644 --- a/skills/docs/scripts/evidence_receipt.py +++ b/skills/docs/scripts/evidence_receipt.py @@ -10,6 +10,7 @@ from pathlib import Path from _docs_checker.evidence import ( + MAX_RECEIPT_BYTES, build_evidence_receipt, canonical_receipt_bytes, observe_entry_orientation, @@ -46,8 +47,15 @@ def _status(root): def _metadata(path): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_RECEIPT_BYTES + 1) + except OSError as exc: + raise ValueError("metadata file is unavailable or malformed") from exc + if len(raw) > MAX_RECEIPT_BYTES: + raise ValueError("metadata file exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("metadata file is unavailable or malformed") from exc expected = {"receipt_id", "repository_identifier", "run", "semantic", "unresolved", "doctor"} if not isinstance(value, dict) or set(value) != expected: @@ -103,7 +111,14 @@ def main(argv=None): ) sys.stdout.buffer.write(canonical_receipt_bytes(receipt)) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError, RecursionError): + print( + json.dumps( + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None} + ) + ) + return 2 + except ValueError as exc: print( json.dumps( {"status": "failed", "error": str(exc), "receipt": None}, diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index dd5be51..e3e1de8 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -1,4 +1,6 @@ import copy +import contextlib +import io import json import os import subprocess @@ -6,6 +8,7 @@ import tempfile import unittest from pathlib import Path +from unittest import mock from urllib.parse import quote @@ -19,6 +22,7 @@ from _docs_checker.health import HEALTH_RUBRIC_VERSION, HEALTH_WEIGHTS import prepare_docs_corpus import run_docs_corpus +import evidence_receipt as evidence_receipt_cli DOGFOOD = ROOT / "evals" / "dogfood" / "cline-0.1.3.json" @@ -137,13 +141,17 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): } with self.subTest(target=target), self.assertRaises(ValueError): evidence.validate_evidence_receipt(value) + with self.subTest(target=f"canonical-{target}"), self.assertRaises(ValueError): + evidence.canonical_receipt_bytes(value) - value = copy.deepcopy(self.receipt) - value["evidence"]["deterministic"]["findings"][0]["target"] = { - "status": "completed", - "value": "/docs/safe-guide#api:v1", - } - evidence.validate_evidence_receipt(value) + for target in ("/docs/safe-guide#api:v1", "/docs/safe-guide#file:v1"): + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["target"] = { + "status": "completed", + "value": target, + } + with self.subTest(target=target): + evidence.validate_evidence_receipt(value) for credential in ( "sk-proj-synthetic012345678901234567890", @@ -151,6 +159,21 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): "npm_012345678901234567890123456789012345", "pypi-synthetic012345678901234567890", "eyJsynthetic01.eyJsynthetic02.synthetic-signature", + "".join(("sk", "_live_", "synthetic012345678901234567890")), + "".join(("rk", "_live_", "synthetic012345678901234567890")), + "lin_api_synthetic012345678901234567890", + "ya29.synthetic012345678901234567890", + "SG.synthetic012345.synthetic01234567890", + "hf_synthetic012345678901234567890", + "sk-ant-synthetic012345678901234567890", + "gsk_synthetic0123456789012345678901234567890", + "r8_0123456789012345678901234567890123456", + "shpat_synthetic012345678901234567890", + "sq0atp-synthetic012345678901234567890", + "dop_v1_synthetic012345678901234567890", + "vercel_synthetic012345678901234567890", + "sbp_synthetic012345678901234567890", + "ASIA0123456789ABCDEF", ): value = copy.deepcopy(self.receipt) value["run"]["model"] = credential @@ -158,6 +181,10 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): ValueError, "credential-shaped" ): evidence.validate_evidence_receipt(value) + with self.subTest(credential=f"canonical-{credential}"), self.assertRaisesRegex( + ValueError, "credential-shaped" + ): + evidence.canonical_receipt_bytes(value) for private_path in ( ".local/private.md", @@ -187,6 +214,26 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): with self.subTest(target=target), self.assertRaises(ValueError): evidence.validate_evidence_receipt(value) + for target in ( + "", + "", + " ", + ' "title"', + ' "contact"', + "<>", + "title data:text/plain,private", + "docs/(data:text/plain,private)", + ): + value = copy.deepcopy(self.receipt) + value["evidence"]["deterministic"]["findings"][0]["target"] = { + "status": "completed", + "value": target, + } + with self.subTest(target=target), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + with self.subTest(target=f"canonical-{target}"), self.assertRaises(ValueError): + evidence.canonical_receipt_bytes(value) + def test_completed_health_requires_every_category(self): value = copy.deepcopy(self.receipt) del value["health"]["categories"]["titles"] @@ -233,16 +280,44 @@ def test_builder_counts_hidden_pages_separately_from_pages(self): navigation = copy.deepcopy(measurements["navigation"]) navigation["navigated_pages"] = ["docs/README.md", "docs/guide.md"] navigation["hidden_pages"] = ["docs/guide.md"] - receipt = evidence.build_evidence_receipt( - receipt_id="page-count-regression", - repository_identifier="example.invalid/docs/page-count", - commit="0" * 40, - checker_version="0.1.4", - run=copy.deepcopy(self.receipt["run"]), - checker_payload={"navigation": navigation, "health": health, "findings": findings}, - orientation=copy.deepcopy(self.receipt["orientation"]), - semantic=run_docs_corpus._semantic_not_assessed(), - ) + builder = { + "receipt_id": "page-count-regression", + "repository_identifier": "example.invalid/docs/page-count", + "commit": "0" * 40, + "checker_version": "0.1.4", + "run": copy.deepcopy(self.receipt["run"]), + "checker_payload": { + "navigation": navigation, + "health": health, + "findings": findings, + }, + "orientation": copy.deepcopy(self.receipt["orientation"]), + "semantic": run_docs_corpus._semantic_not_assessed(), + } + receipt = evidence.build_evidence_receipt(**builder) + + for field, malformed in ( + ("run", None), + ("semantic", {}), + ( + "semantic", + {"status": "not_assessed", "evaluator": None, "findings": []}, + ), + ( + "semantic", + { + "status": "not_assessed", + "evaluator": run_docs_corpus._semantic_not_assessed()["evaluator"], + "findings": None, + }, + ), + ("unresolved", None), + ("doctor", None), + ("doctor", {}), + ): + malformed_builder = {**builder, field: malformed} + with self.subTest(field=field, malformed=malformed), self.assertRaises(ValueError): + evidence.build_evidence_receipt(**malformed_builder) self.assertEqual(receipt["counts"]["pages"], {"status": "completed", "value": 2}) self.assertEqual(receipt["counts"]["hidden_pages"], {"status": "completed", "value": 1}) @@ -254,6 +329,24 @@ def test_unavailable_is_not_zero_and_must_match_index(self): with self.assertRaisesRegex(ValueError, "unavailable_evidence"): evidence.validate_evidence_receipt(value) + def test_deep_receipt_input_fails_with_controlled_validation_error(self): + deeply_nested = [] + current = deeply_nested + for _ in range(1_100): + child = [] + current.append(child) + current = child + value = copy.deepcopy(self.receipt) + value["evidence"]["semantic"]["findings"] = deeply_nested + with self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + + for field, malformed in (("semantic", []), ("unresolved", {})): + value = copy.deepcopy(self.receipt) + value["evidence"][field]["status"] = malformed + with self.subTest(field=field), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + def test_orientation_is_inert_and_does_not_change_rubric(self): with tempfile.TemporaryDirectory() as td: root = Path(td) @@ -332,6 +425,53 @@ def test_stdout_receipt_entrypoint_combines_one_checker_run(self): self.assertEqual(receipt["health"]["status"], "completed") self.assertEqual(receipt["write_audit"]["writes_observed"]["value"], 0) + def test_metadata_input_is_bounded_before_json_parse(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + metadata = root / "oversized.json" + metadata.write_bytes(b" " * (evidence.MAX_RECEIPT_BYTES + 1)) + with self.assertRaisesRegex(ValueError, "exceeds capacity"): + evidence_receipt_cli._metadata(metadata) + + deeply_nested = root / "deeply-nested.json" + deeply_nested.write_text("[" * 2_000 + "]" * 2_000, encoding="utf-8") + with self.assertRaises(ValueError): + evidence_receipt_cli._metadata(deeply_nested) + + def test_receipt_cli_io_failure_does_not_expose_private_path(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + metadata = root / "metadata.json" + metadata.write_text( + json.dumps( + { + "receipt_id": "io-failure", + "repository_identifier": "example.invalid/docs/io-failure", + "run": copy.deepcopy(self.receipt["run"]), + "semantic": run_docs_corpus._semantic_not_assessed(), + "unresolved": [], + "doctor": copy.deepcopy(self.receipt["doctor"]), + } + ), + encoding="utf-8", + ) + private = Path(r"C:\Users\Synthetic\private-repo\docs\README.md") + output = io.StringIO() + with mock.patch.object(evidence_receipt_cli, "_git", return_value=""), mock.patch.object( + evidence_receipt_cli, + "check", + side_effect=OSError(13, "access denied", str(private)), + ), contextlib.redirect_stdout(output): + self.assertEqual( + evidence_receipt_cli.main([str(root), "--metadata-file", str(metadata)]), + 2, + ) + self.assertEqual( + json.loads(output.getvalue()), + {"status": "failed", "error": "evidence receipt I/O failed", "receipt": None}, + ) + self.assertNotIn(str(private), output.getvalue()) + class CorpusHarnessTests(unittest.TestCase): def test_manifest_has_six_exact_immutable_pins(self): @@ -347,6 +487,87 @@ def test_manifest_has_six_exact_immutable_pins(self): self.assertTrue(row["config_probes"]) self.assertTrue(row["sparse_paths"]) + def test_manifest_input_is_bounded_before_parse_and_iteration(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + oversized = root / "oversized.json" + oversized.write_bytes(b" " * (run_docs_corpus.MAX_MANIFEST_BYTES + 1)) + with self.assertRaisesRegex(ValueError, "exceeds capacity"): + run_docs_corpus.load_manifest(oversized) + + deeply_nested = root / "deeply-nested.json" + deeply_nested.write_text("[" * 2_000 + "]" * 2_000, encoding="utf-8") + with self.assertRaises(ValueError): + run_docs_corpus.load_manifest(deeply_nested) + + for malformed in ({}, ["nested"], 42, None): + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["config_probes"] = [malformed] + malformed_path = root / f"malformed-{type(malformed).__name__}.json" + malformed_path.write_text(json.dumps(manifest), encoding="utf-8") + with self.subTest(malformed=malformed), self.assertRaisesRegex( + ValueError, "config_probes is invalid" + ): + run_docs_corpus.load_manifest(malformed_path) + + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["provider"] = {} + malformed_provider = root / "malformed-provider.json" + malformed_provider.write_text(json.dumps(manifest), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "provider is invalid"): + run_docs_corpus.load_manifest(malformed_provider) + + for control_path in ("docs\n/*", "docs\r\n!/*", "docs\tprivate", "docs\x00private"): + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["sparse_paths"] = [control_path] + control_manifest = root / f"control-{len(control_path)}.json" + control_manifest.write_text(json.dumps(manifest), encoding="utf-8") + with self.subTest(control_path=control_path), self.assertRaisesRegex( + ValueError, "control characters" + ): + run_docs_corpus.load_manifest(control_manifest) + + for pattern_path in ("*", "docs/**", "docs/[ab]", "!docs/private"): + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["sparse_paths"] = [pattern_path] + pattern_manifest = root / f"pattern-{len(pattern_path)}.json" + pattern_manifest.write_text(json.dumps(manifest), encoding="utf-8") + with self.subTest(pattern_path=pattern_path), self.assertRaisesRegex( + ValueError, "contains pattern syntax" + ): + run_docs_corpus.load_manifest(pattern_manifest) + + for private_path in ( + ".local/private-config.json", + "docs/sk-live-abcdefghijklmnop.json", + "docs/%252elocal/private-config.json", + ): + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["config_probes"] = [private_path] + private_manifest = root / f"private-{len(private_path)}.json" + private_manifest.write_text(json.dumps(manifest), encoding="utf-8") + with self.subTest(private_path=private_path), self.assertRaises(ValueError): + run_docs_corpus.load_manifest(private_manifest) + + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["config_probes"] = [ + f"docs/config-{index}.json" + for index in range(run_docs_corpus.MAX_PROBES_PER_REPOSITORY + 1) + ] + too_many = root / "too-many.json" + too_many.write_text(json.dumps(manifest), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "config_probes is invalid"): + run_docs_corpus.load_manifest(too_many) + + manifest = json.loads(CORPUS.read_text(encoding="utf-8")) + manifest["repositories"][0]["entry"] = ( + "docs/" + "x" * run_docs_corpus.MAX_MANIFEST_PATH_BYTES + ) + long_path = root / "long-path.json" + long_path.write_text(json.dumps(manifest), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "exceeds capacity"): + run_docs_corpus.load_manifest(long_path) + def test_checked_in_baseline_separates_supported_and_unavailable_evidence(self): baseline = json.loads(BASELINE.read_text(encoding="utf-8")) self.assertEqual(baseline["checker_version"], "0.1.4") @@ -397,6 +618,33 @@ def test_unsupported_provider_is_observed_but_not_scored_or_executed(self): self.assertEqual(result["configurations"][0]["status"], "completed") self.assertTrue(result["configurations"][0]["sha256"].startswith("sha256:")) + def test_configuration_paths_are_sanitized_before_outer_corpus_output(self): + with tempfile.TemporaryDirectory() as td: + workspace = Path(td) + spec = { + "id": "vite", + "repository_url": "https://github.com/vitejs/vite.git", + "commit": "0" * 40, + "provider": "vitepress", + "measurement": "unsupported", + "scope": "docs", + "entry": "docs/index.md", + "authority_probes": ["docs/.vitepress/config.ts"], + "config_probes": ["docs/sk-live-abcdefghijklmnop.json"], + "sparse_paths": ["docs"], + } + _fixture_checkout( + workspace, + spec, + { + "docs/index.md": "# Vite\n", + "docs/.vitepress/config.ts": "export default {}\n", + spec["config_probes"][0]: "inert\n", + }, + ) + with self.assertRaisesRegex(ValueError, "credential-shaped"): + run_docs_corpus.run_repository(workspace, spec) + def test_checkout_verification_rejects_branch_dirty_and_wrong_commit(self): with tempfile.TemporaryDirectory() as td: workspace = Path(td) @@ -452,6 +700,85 @@ def test_prepare_refuses_unowned_or_existing_workspace(self): finally: prepare_docs_corpus.WORKSPACE_ROOT = original + def test_prepare_manifest_and_marker_reads_are_bounded(self): + with tempfile.TemporaryDirectory() as td: + base = Path(td) + original = prepare_docs_corpus.WORKSPACE_ROOT + prepare_docs_corpus.WORKSPACE_ROOT = base + try: + oversized_manifest = base / "oversized-manifest.json" + oversized_manifest.write_bytes(b" " * (run_docs_corpus.MAX_MANIFEST_BYTES + 1)) + with self.assertRaisesRegex(ValueError, "manifest exceeds capacity"): + prepare_docs_corpus._workspace( + base / "manifest-workspace", + oversized_manifest, + "docs-corpus-v1", + ) + + marker_workspace = base / "marker-workspace" + marker_workspace.mkdir() + (marker_workspace / prepare_docs_corpus.MARKER).write_bytes( + b" " * (prepare_docs_corpus.MAX_MARKER_BYTES + 1) + ) + with self.assertRaises(ValueError): + prepare_docs_corpus._workspace( + marker_workspace, + CORPUS, + "docs-corpus-v1", + ) + + deep_marker_workspace = base / "deep-marker-workspace" + deep_marker_workspace.mkdir() + (deep_marker_workspace / prepare_docs_corpus.MARKER).write_text( + "[" * 2_000 + "]" * 2_000, + encoding="utf-8", + ) + with self.assertRaises(ValueError): + prepare_docs_corpus._workspace( + deep_marker_workspace, + CORPUS, + "docs-corpus-v1", + ) + finally: + prepare_docs_corpus.WORKSPACE_ROOT = original + + def test_prepare_command_failure_does_not_expose_checkout_path(self): + private = Path(r"C:\Users\Synthetic\private\docs-corpus") + failed = mock.Mock(returncode=1) + with mock.patch.object(prepare_docs_corpus.subprocess, "run", return_value=failed): + with self.assertRaisesRegex(ValueError, "initialize repository") as raised: + prepare_docs_corpus._run( + ["git", "init", str(private)], + operation="initialize repository", + ) + self.assertNotIn(str(private), str(raised.exception)) + + output = io.StringIO() + with mock.patch.object( + prepare_docs_corpus, + "prepare", + side_effect=OSError(f"access denied: {private}"), + ), contextlib.redirect_stdout(output): + self.assertEqual(prepare_docs_corpus.main([]), 2) + self.assertEqual( + json.loads(output.getvalue()), + {"status": "failed", "error": "corpus preparation I/O failed"}, + ) + self.assertNotIn(str(private), output.getvalue()) + + output = io.StringIO() + with mock.patch.object( + run_docs_corpus, + "run_corpus", + side_effect=OSError(f"access denied: {private}"), + ), contextlib.redirect_stdout(output): + self.assertEqual(run_docs_corpus.main([]), 2) + self.assertEqual( + json.loads(output.getvalue()), + {"status": "failed", "error": "corpus runner I/O failed"}, + ) + self.assertNotIn(str(private), output.getvalue()) + def test_runner_output_cannot_write_into_workspace_or_through_reparse(self): with tempfile.TemporaryDirectory() as td: root = Path(td) diff --git a/tools/prepare_docs_corpus.py b/tools/prepare_docs_corpus.py index 6e19ace..fc7b313 100644 --- a/tools/prepare_docs_corpus.py +++ b/tools/prepare_docs_corpus.py @@ -13,6 +13,7 @@ from run_docs_corpus import ( DEFAULT_MANIFEST, DEFAULT_WORKSPACE, + MAX_MANIFEST_BYTES, ROOT, _assert_no_reparse_components, load_manifest, @@ -23,9 +24,21 @@ WORKSPACE_ROOT = ROOT / "evals" / "workspace" MARKER = ".docs-corpus-owner.json" +MAX_MARKER_BYTES = 4 * 1024 -def _run(command, *, input_text=None): +def _bounded_bytes(path, maximum, name): + try: + with Path(path).open("rb") as stream: + raw = stream.read(maximum + 1) + except OSError as exc: + raise ValueError(f"{name} is unavailable") from exc + if len(raw) > maximum: + raise ValueError(f"{name} exceeds capacity") + return raw + + +def _run(command, *, operation, input_text=None): completed = subprocess.run( command, input=input_text, @@ -36,7 +49,7 @@ def _run(command, *, input_text=None): check=False, ) if completed.returncode: - raise ValueError(f"corpus acquisition failed: {' '.join(command[:4])}") + raise ValueError(f"corpus acquisition failed: {operation}") return completed @@ -58,12 +71,15 @@ def _workspace(path, manifest_path, corpus_id): workspace.mkdir(parents=True, exist_ok=True) workspace = safe_path(workspace, root) marker = safe_path(workspace / MARKER, workspace) - digest = hashlib.sha256(Path(manifest_path).read_bytes()).hexdigest() + digest = hashlib.sha256( + _bounded_bytes(manifest_path, MAX_MANIFEST_BYTES, "corpus manifest") + ).hexdigest() expected = {"corpus_id": corpus_id, "manifest_sha256": f"sha256:{digest}"} if marker.exists(): try: - current = json.loads(marker.read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + marker_raw = _bounded_bytes(marker, MAX_MARKER_BYTES, "corpus workspace marker") + current = json.loads(marker_raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("corpus workspace marker is malformed") from exc if current != expected: raise ValueError("corpus workspace marker does not match") @@ -92,11 +108,18 @@ def prepare(manifest=DEFAULT_MANIFEST, workspace=DEFAULT_WORKSPACE, repository_i target = safe_path(workspace / spec["id"], workspace) if target.exists(): raise ValueError(f"refusing to update or reuse corpus repository: {spec['id']}") - _run(["git", "init", str(target)]) - _run(["git", "-C", str(target), "remote", "add", "origin", spec["repository_url"]]) - _run(["git", "-C", str(target), "sparse-checkout", "init", "--no-cone"]) + _run(["git", "init", str(target)], operation="initialize repository") + _run( + ["git", "-C", str(target), "remote", "add", "origin", spec["repository_url"]], + operation="configure repository remote", + ) + _run( + ["git", "-C", str(target), "sparse-checkout", "init", "--no-cone"], + operation="initialize sparse checkout", + ) _run( ["git", "-C", str(target), "sparse-checkout", "set", "--no-cone", "--stdin"], + operation="configure sparse checkout", input_text="\n".join(spec["sparse_paths"]) + "\n", ) _run( @@ -109,9 +132,13 @@ def prepare(manifest=DEFAULT_MANIFEST, workspace=DEFAULT_WORKSPACE, repository_i "--filter=blob:none", "origin", spec["commit"], - ] + ], + operation="fetch pinned commit", + ) + _run( + ["git", "-C", str(target), "checkout", "--detach", "FETCH_HEAD"], + operation="checkout pinned commit", ) - _run(["git", "-C", str(target), "checkout", "--detach", "FETCH_HEAD"]) verify_checkout(workspace, spec) rows.append({"id": spec["id"], "commit": spec["commit"], "status": "prepared"}) return {"status": "completed", "corpus_id": manifest_value["corpus_id"], "repositories": rows} @@ -133,7 +160,10 @@ def main(argv=None): ) ) return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError): + print(json.dumps({"status": "failed", "error": "corpus preparation I/O failed"})) + return 2 + except ValueError as exc: print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=True)) return 2 diff --git a/tools/run_docs_corpus.py b/tools/run_docs_corpus.py index 0fd7d1a..60705fa 100644 --- a/tools/run_docs_corpus.py +++ b/tools/run_docs_corpus.py @@ -22,18 +22,24 @@ config_probe, evidence_value, observe_entry_orientation, + validate_relative_evidence_path, ) from _docs_checker.health import HEALTH_RUBRIC_VERSION, HEALTH_WEIGHTS, health_summary # noqa: E402 from _docs_checker.init_adoption import SKILL_VERSION # noqa: E402 -from _docs_checker.paths import _assert_no_reparse_components, normalize_repo_relative, safe_path # noqa: E402 +from _docs_checker.paths import _assert_no_reparse_components, safe_path # noqa: E402 from check import check # noqa: E402 DEFAULT_MANIFEST = ROOT / "evals" / "docs-corpus-v1.json" DEFAULT_WORKSPACE = ROOT / "evals" / "workspace" / "docs-corpus-v1" +MAX_MANIFEST_BYTES = 256 * 1024 +MAX_MANIFEST_PATH_BYTES = 512 +MAX_PROBES_PER_REPOSITORY = 64 +MAX_SPARSE_PATHS_PER_REPOSITORY = 128 _SHA = re.compile(r"^[0-9a-f]{40}$") _ID = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$") _PROVIDERS = frozenset({"mintlify", "custom-mdx", "docusaurus", "vitepress", "mkdocs", "hugo"}) +_SPARSE_PATTERN_SYNTAX = re.compile(r"[*?!\[\]]") def _git(root, *args, allowed=(0,)): @@ -51,16 +57,23 @@ def _git(root, *args, allowed=(0,)): def _relative(value, name): - normalized = normalize_repo_relative(value, name) - if normalized != value: - raise ValueError(f"{name} must be normalized") + normalized = validate_relative_evidence_path(value, name) + if len(value.encode("utf-8", "strict")) > MAX_MANIFEST_PATH_BYTES: + raise ValueError(f"{name} exceeds capacity") return normalized def load_manifest(path=DEFAULT_MANIFEST): try: - value = json.loads(Path(path).read_text(encoding="utf-8")) - except (OSError, UnicodeError, ValueError) as exc: + with Path(path).open("rb") as stream: + raw = stream.read(MAX_MANIFEST_BYTES + 1) + except OSError as exc: + raise ValueError("corpus manifest is unavailable or malformed") from exc + if len(raw) > MAX_MANIFEST_BYTES: + raise ValueError("corpus manifest exceeds capacity") + try: + value = json.loads(raw.decode("utf-8", "strict")) + except (UnicodeError, ValueError, RecursionError) as exc: raise ValueError("corpus manifest is unavailable or malformed") from exc if not isinstance(value, dict) or set(value) != {"schema_version", "corpus_id", "repositories"}: raise ValueError("corpus manifest fields are invalid") @@ -98,7 +111,7 @@ def load_manifest(path=DEFAULT_MANIFEST): raise ValueError(f"{name}.repository_url is invalid") if not isinstance(spec["commit"], str) or _SHA.fullmatch(spec["commit"]) is None: raise ValueError(f"{name}.commit is invalid") - if spec["provider"] not in _PROVIDERS: + if not isinstance(spec["provider"], str) or spec["provider"] not in _PROVIDERS: raise ValueError(f"{name}.provider is invalid") expected_measurement = "supported" if spec["provider"] == "mintlify" else "unsupported" if spec["measurement"] != expected_measurement: @@ -107,10 +120,23 @@ def load_manifest(path=DEFAULT_MANIFEST): _relative(spec["entry"], f"{name}.entry") for field in ("authority_probes", "config_probes", "sparse_paths"): paths = spec[field] - if not isinstance(paths, list) or not paths or len(paths) != len(set(paths)): + maximum = ( + MAX_SPARSE_PATHS_PER_REPOSITORY + if field == "sparse_paths" + else MAX_PROBES_PER_REPOSITORY + ) + if ( + not isinstance(paths, list) + or not paths + or len(paths) > maximum + or any(not isinstance(path, str) for path in paths) + or len(paths) != len(set(paths)) + ): raise ValueError(f"{name}.{field} is invalid") for path_index, path in enumerate(paths): _relative(path, f"{name}.{field}[{path_index}]") + if field == "sparse_paths" and _SPARSE_PATTERN_SYNTAX.search(path): + raise ValueError(f"{name}.{field}[{path_index}] contains pattern syntax") return value @@ -181,6 +207,8 @@ def _unsupported_payload(spec): def run_repository(workspace, spec): + for index, relative in enumerate(spec["config_probes"]): + _relative(relative, f"{spec['id']}.config_probes[{index}]") path, before_raw = verify_checkout(workspace, spec) configurations = [] for relative in spec["config_probes"]: @@ -300,7 +328,10 @@ def main(argv=None): else: print(encoded, end="") return 0 - except (OSError, UnicodeError, ValueError) as exc: + except (OSError, UnicodeError): + print(json.dumps({"status": "failed", "error": "corpus runner I/O failed"})) + return 2 + except ValueError as exc: print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=True)) return 2 From c952a746c59c4e467f89e5f5badf0376353bff2a Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 04:06:28 -0400 Subject: [PATCH 05/20] Address 0.1.4 PR review findings --- .../docs/scripts/_docs_checker/evidence.py | 113 +++++++++++-- .../copilot/scripts/_docs_checker/evidence.py | 113 +++++++++++-- .../cursor/scripts/_docs_checker/evidence.py | 113 +++++++++++-- .../grok/scripts/_docs_checker/evidence.py | 113 +++++++++++-- .../docs/scripts/_docs_checker/evidence.py | 113 +++++++++++-- skills/docs/scripts/_docs_checker/evidence.py | 113 +++++++++++-- tests/test_docs_evidence.py | 160 ++++++++++++++++++ tools/prepare_docs_corpus.py | 11 +- tools/run_docs_corpus.py | 4 + 9 files changed, 774 insertions(+), 79 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index e3844f7..4ead0b2 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -525,6 +525,62 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _markdown_body_lines(text): + """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" + lines = text.removeprefix("\ufeff").splitlines(keepends=True) + if not lines or lines[0].strip() != "---": + return lines + region_bytes = 0 + for index, line in enumerate(lines): + region_bytes += len(line.encode("utf-8", "strict")) + if region_bytes > MAX_FRONTMATTER_BYTES: + return None + delimiter = line.rstrip("\r\n") + if ( + index + and not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in {"---", "..."} + ): + return lines[index + 1 :] + return None + + +def _after_leading_comments(line, in_html_comment, in_mdx_comment): + """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" + current = line.lstrip() + if in_html_comment: + return "", "-->" not in current, in_mdx_comment + if in_mdx_comment: + return "", in_html_comment, "*/}" not in current + if current.startswith("" not in current[4:], in_mdx_comment + if current.startswith("{/*"): + return "", in_html_comment, "*/}" not in current[3:] + return current, in_html_comment, in_mdx_comment + + +def _fence_marker(line): + """Return a CommonMark-style fence marker and suffix for up to three spaces.""" + match = re.match(r"^( {0,3})(`{3,}|~{3,})(.*)$", line.rstrip("\r\n")) + if match is None: + return None + marker = match.group(2) + return marker[0], len(marker), match.group(3) + + +def _leading_indent_columns(line): + """Count leading Markdown indentation columns with four-column tab stops.""" + columns = 0 + for char in line: + if char == " ": + columns += 1 + elif char == "\t": + columns += 4 - (columns % 4) + else: + break + return columns + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -546,22 +602,53 @@ def observe_entry_orientation(root, entry): "frontmatter_title": evidence_value("unavailable"), "provider_rendered_title": evidence_value("unavailable"), } - lines = text.removeprefix("\ufeff").splitlines() - in_fence = False - literal_h1 = False - for line in lines: - stripped = line.lstrip() - if stripped.startswith(("```", "~~~")): - in_fence = not in_fence - continue - if not in_fence and re.match(r"^#(?:\s+|$)", stripped): - literal_h1 = True - break + body_lines = _markdown_body_lines(text) + literal_h1 = None + if body_lines is not None: + fence = None + in_html_comment = False + in_mdx_comment = False + literal_h1 = False + for line in body_lines: + marker = _fence_marker(line) + if fence is not None: + if ( + marker is not None + and marker[0] == fence[0] + and marker[1] >= fence[1] + and not marker[2].strip() + ): + fence = None + continue + indented_code = _leading_indent_columns(line) >= 4 + if indented_code and not (in_html_comment or in_mdx_comment): + continue + stripped, in_html_comment, in_mdx_comment = _after_leading_comments( + line, in_html_comment, in_mdx_comment + ) + if not stripped: + continue + if marker is not None: + if marker[0] != "`" or "`" not in marker[2]: + fence = (marker[0], marker[1]) + continue + if re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { - "literal_h1": evidence_value("completed", literal_h1), - "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "literal_h1": evidence_value("completed", literal_h1) + if literal_h1 is not None + else evidence_value("unavailable"), + "frontmatter_title": frontmatter_title, "provider_rendered_title": evidence_value("unavailable"), } diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index e3844f7..4ead0b2 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -525,6 +525,62 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _markdown_body_lines(text): + """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" + lines = text.removeprefix("\ufeff").splitlines(keepends=True) + if not lines or lines[0].strip() != "---": + return lines + region_bytes = 0 + for index, line in enumerate(lines): + region_bytes += len(line.encode("utf-8", "strict")) + if region_bytes > MAX_FRONTMATTER_BYTES: + return None + delimiter = line.rstrip("\r\n") + if ( + index + and not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in {"---", "..."} + ): + return lines[index + 1 :] + return None + + +def _after_leading_comments(line, in_html_comment, in_mdx_comment): + """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" + current = line.lstrip() + if in_html_comment: + return "", "-->" not in current, in_mdx_comment + if in_mdx_comment: + return "", in_html_comment, "*/}" not in current + if current.startswith("" not in current[4:], in_mdx_comment + if current.startswith("{/*"): + return "", in_html_comment, "*/}" not in current[3:] + return current, in_html_comment, in_mdx_comment + + +def _fence_marker(line): + """Return a CommonMark-style fence marker and suffix for up to three spaces.""" + match = re.match(r"^( {0,3})(`{3,}|~{3,})(.*)$", line.rstrip("\r\n")) + if match is None: + return None + marker = match.group(2) + return marker[0], len(marker), match.group(3) + + +def _leading_indent_columns(line): + """Count leading Markdown indentation columns with four-column tab stops.""" + columns = 0 + for char in line: + if char == " ": + columns += 1 + elif char == "\t": + columns += 4 - (columns % 4) + else: + break + return columns + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -546,22 +602,53 @@ def observe_entry_orientation(root, entry): "frontmatter_title": evidence_value("unavailable"), "provider_rendered_title": evidence_value("unavailable"), } - lines = text.removeprefix("\ufeff").splitlines() - in_fence = False - literal_h1 = False - for line in lines: - stripped = line.lstrip() - if stripped.startswith(("```", "~~~")): - in_fence = not in_fence - continue - if not in_fence and re.match(r"^#(?:\s+|$)", stripped): - literal_h1 = True - break + body_lines = _markdown_body_lines(text) + literal_h1 = None + if body_lines is not None: + fence = None + in_html_comment = False + in_mdx_comment = False + literal_h1 = False + for line in body_lines: + marker = _fence_marker(line) + if fence is not None: + if ( + marker is not None + and marker[0] == fence[0] + and marker[1] >= fence[1] + and not marker[2].strip() + ): + fence = None + continue + indented_code = _leading_indent_columns(line) >= 4 + if indented_code and not (in_html_comment or in_mdx_comment): + continue + stripped, in_html_comment, in_mdx_comment = _after_leading_comments( + line, in_html_comment, in_mdx_comment + ) + if not stripped: + continue + if marker is not None: + if marker[0] != "`" or "`" not in marker[2]: + fence = (marker[0], marker[1]) + continue + if re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { - "literal_h1": evidence_value("completed", literal_h1), - "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "literal_h1": evidence_value("completed", literal_h1) + if literal_h1 is not None + else evidence_value("unavailable"), + "frontmatter_title": frontmatter_title, "provider_rendered_title": evidence_value("unavailable"), } diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index e3844f7..4ead0b2 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -525,6 +525,62 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _markdown_body_lines(text): + """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" + lines = text.removeprefix("\ufeff").splitlines(keepends=True) + if not lines or lines[0].strip() != "---": + return lines + region_bytes = 0 + for index, line in enumerate(lines): + region_bytes += len(line.encode("utf-8", "strict")) + if region_bytes > MAX_FRONTMATTER_BYTES: + return None + delimiter = line.rstrip("\r\n") + if ( + index + and not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in {"---", "..."} + ): + return lines[index + 1 :] + return None + + +def _after_leading_comments(line, in_html_comment, in_mdx_comment): + """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" + current = line.lstrip() + if in_html_comment: + return "", "-->" not in current, in_mdx_comment + if in_mdx_comment: + return "", in_html_comment, "*/}" not in current + if current.startswith("" not in current[4:], in_mdx_comment + if current.startswith("{/*"): + return "", in_html_comment, "*/}" not in current[3:] + return current, in_html_comment, in_mdx_comment + + +def _fence_marker(line): + """Return a CommonMark-style fence marker and suffix for up to three spaces.""" + match = re.match(r"^( {0,3})(`{3,}|~{3,})(.*)$", line.rstrip("\r\n")) + if match is None: + return None + marker = match.group(2) + return marker[0], len(marker), match.group(3) + + +def _leading_indent_columns(line): + """Count leading Markdown indentation columns with four-column tab stops.""" + columns = 0 + for char in line: + if char == " ": + columns += 1 + elif char == "\t": + columns += 4 - (columns % 4) + else: + break + return columns + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -546,22 +602,53 @@ def observe_entry_orientation(root, entry): "frontmatter_title": evidence_value("unavailable"), "provider_rendered_title": evidence_value("unavailable"), } - lines = text.removeprefix("\ufeff").splitlines() - in_fence = False - literal_h1 = False - for line in lines: - stripped = line.lstrip() - if stripped.startswith(("```", "~~~")): - in_fence = not in_fence - continue - if not in_fence and re.match(r"^#(?:\s+|$)", stripped): - literal_h1 = True - break + body_lines = _markdown_body_lines(text) + literal_h1 = None + if body_lines is not None: + fence = None + in_html_comment = False + in_mdx_comment = False + literal_h1 = False + for line in body_lines: + marker = _fence_marker(line) + if fence is not None: + if ( + marker is not None + and marker[0] == fence[0] + and marker[1] >= fence[1] + and not marker[2].strip() + ): + fence = None + continue + indented_code = _leading_indent_columns(line) >= 4 + if indented_code and not (in_html_comment or in_mdx_comment): + continue + stripped, in_html_comment, in_mdx_comment = _after_leading_comments( + line, in_html_comment, in_mdx_comment + ) + if not stripped: + continue + if marker is not None: + if marker[0] != "`" or "`" not in marker[2]: + fence = (marker[0], marker[1]) + continue + if re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { - "literal_h1": evidence_value("completed", literal_h1), - "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "literal_h1": evidence_value("completed", literal_h1) + if literal_h1 is not None + else evidence_value("unavailable"), + "frontmatter_title": frontmatter_title, "provider_rendered_title": evidence_value("unavailable"), } diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index e3844f7..4ead0b2 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -525,6 +525,62 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _markdown_body_lines(text): + """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" + lines = text.removeprefix("\ufeff").splitlines(keepends=True) + if not lines or lines[0].strip() != "---": + return lines + region_bytes = 0 + for index, line in enumerate(lines): + region_bytes += len(line.encode("utf-8", "strict")) + if region_bytes > MAX_FRONTMATTER_BYTES: + return None + delimiter = line.rstrip("\r\n") + if ( + index + and not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in {"---", "..."} + ): + return lines[index + 1 :] + return None + + +def _after_leading_comments(line, in_html_comment, in_mdx_comment): + """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" + current = line.lstrip() + if in_html_comment: + return "", "-->" not in current, in_mdx_comment + if in_mdx_comment: + return "", in_html_comment, "*/}" not in current + if current.startswith("" not in current[4:], in_mdx_comment + if current.startswith("{/*"): + return "", in_html_comment, "*/}" not in current[3:] + return current, in_html_comment, in_mdx_comment + + +def _fence_marker(line): + """Return a CommonMark-style fence marker and suffix for up to three spaces.""" + match = re.match(r"^( {0,3})(`{3,}|~{3,})(.*)$", line.rstrip("\r\n")) + if match is None: + return None + marker = match.group(2) + return marker[0], len(marker), match.group(3) + + +def _leading_indent_columns(line): + """Count leading Markdown indentation columns with four-column tab stops.""" + columns = 0 + for char in line: + if char == " ": + columns += 1 + elif char == "\t": + columns += 4 - (columns % 4) + else: + break + return columns + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -546,22 +602,53 @@ def observe_entry_orientation(root, entry): "frontmatter_title": evidence_value("unavailable"), "provider_rendered_title": evidence_value("unavailable"), } - lines = text.removeprefix("\ufeff").splitlines() - in_fence = False - literal_h1 = False - for line in lines: - stripped = line.lstrip() - if stripped.startswith(("```", "~~~")): - in_fence = not in_fence - continue - if not in_fence and re.match(r"^#(?:\s+|$)", stripped): - literal_h1 = True - break + body_lines = _markdown_body_lines(text) + literal_h1 = None + if body_lines is not None: + fence = None + in_html_comment = False + in_mdx_comment = False + literal_h1 = False + for line in body_lines: + marker = _fence_marker(line) + if fence is not None: + if ( + marker is not None + and marker[0] == fence[0] + and marker[1] >= fence[1] + and not marker[2].strip() + ): + fence = None + continue + indented_code = _leading_indent_columns(line) >= 4 + if indented_code and not (in_html_comment or in_mdx_comment): + continue + stripped, in_html_comment, in_mdx_comment = _after_leading_comments( + line, in_html_comment, in_mdx_comment + ) + if not stripped: + continue + if marker is not None: + if marker[0] != "`" or "`" not in marker[2]: + fence = (marker[0], marker[1]) + continue + if re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { - "literal_h1": evidence_value("completed", literal_h1), - "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "literal_h1": evidence_value("completed", literal_h1) + if literal_h1 is not None + else evidence_value("unavailable"), + "frontmatter_title": frontmatter_title, "provider_rendered_title": evidence_value("unavailable"), } diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index e3844f7..4ead0b2 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -525,6 +525,62 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _markdown_body_lines(text): + """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" + lines = text.removeprefix("\ufeff").splitlines(keepends=True) + if not lines or lines[0].strip() != "---": + return lines + region_bytes = 0 + for index, line in enumerate(lines): + region_bytes += len(line.encode("utf-8", "strict")) + if region_bytes > MAX_FRONTMATTER_BYTES: + return None + delimiter = line.rstrip("\r\n") + if ( + index + and not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in {"---", "..."} + ): + return lines[index + 1 :] + return None + + +def _after_leading_comments(line, in_html_comment, in_mdx_comment): + """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" + current = line.lstrip() + if in_html_comment: + return "", "-->" not in current, in_mdx_comment + if in_mdx_comment: + return "", in_html_comment, "*/}" not in current + if current.startswith("" not in current[4:], in_mdx_comment + if current.startswith("{/*"): + return "", in_html_comment, "*/}" not in current[3:] + return current, in_html_comment, in_mdx_comment + + +def _fence_marker(line): + """Return a CommonMark-style fence marker and suffix for up to three spaces.""" + match = re.match(r"^( {0,3})(`{3,}|~{3,})(.*)$", line.rstrip("\r\n")) + if match is None: + return None + marker = match.group(2) + return marker[0], len(marker), match.group(3) + + +def _leading_indent_columns(line): + """Count leading Markdown indentation columns with four-column tab stops.""" + columns = 0 + for char in line: + if char == " ": + columns += 1 + elif char == "\t": + columns += 4 - (columns % 4) + else: + break + return columns + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -546,22 +602,53 @@ def observe_entry_orientation(root, entry): "frontmatter_title": evidence_value("unavailable"), "provider_rendered_title": evidence_value("unavailable"), } - lines = text.removeprefix("\ufeff").splitlines() - in_fence = False - literal_h1 = False - for line in lines: - stripped = line.lstrip() - if stripped.startswith(("```", "~~~")): - in_fence = not in_fence - continue - if not in_fence and re.match(r"^#(?:\s+|$)", stripped): - literal_h1 = True - break + body_lines = _markdown_body_lines(text) + literal_h1 = None + if body_lines is not None: + fence = None + in_html_comment = False + in_mdx_comment = False + literal_h1 = False + for line in body_lines: + marker = _fence_marker(line) + if fence is not None: + if ( + marker is not None + and marker[0] == fence[0] + and marker[1] >= fence[1] + and not marker[2].strip() + ): + fence = None + continue + indented_code = _leading_indent_columns(line) >= 4 + if indented_code and not (in_html_comment or in_mdx_comment): + continue + stripped, in_html_comment, in_mdx_comment = _after_leading_comments( + line, in_html_comment, in_mdx_comment + ) + if not stripped: + continue + if marker is not None: + if marker[0] != "`" or "`" not in marker[2]: + fence = (marker[0], marker[1]) + continue + if re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { - "literal_h1": evidence_value("completed", literal_h1), - "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "literal_h1": evidence_value("completed", literal_h1) + if literal_h1 is not None + else evidence_value("unavailable"), + "frontmatter_title": frontmatter_title, "provider_rendered_title": evidence_value("unavailable"), } diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index e3844f7..4ead0b2 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -525,6 +525,62 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _markdown_body_lines(text): + """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" + lines = text.removeprefix("\ufeff").splitlines(keepends=True) + if not lines or lines[0].strip() != "---": + return lines + region_bytes = 0 + for index, line in enumerate(lines): + region_bytes += len(line.encode("utf-8", "strict")) + if region_bytes > MAX_FRONTMATTER_BYTES: + return None + delimiter = line.rstrip("\r\n") + if ( + index + and not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in {"---", "..."} + ): + return lines[index + 1 :] + return None + + +def _after_leading_comments(line, in_html_comment, in_mdx_comment): + """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" + current = line.lstrip() + if in_html_comment: + return "", "-->" not in current, in_mdx_comment + if in_mdx_comment: + return "", in_html_comment, "*/}" not in current + if current.startswith("" not in current[4:], in_mdx_comment + if current.startswith("{/*"): + return "", in_html_comment, "*/}" not in current[3:] + return current, in_html_comment, in_mdx_comment + + +def _fence_marker(line): + """Return a CommonMark-style fence marker and suffix for up to three spaces.""" + match = re.match(r"^( {0,3})(`{3,}|~{3,})(.*)$", line.rstrip("\r\n")) + if match is None: + return None + marker = match.group(2) + return marker[0], len(marker), match.group(3) + + +def _leading_indent_columns(line): + """Count leading Markdown indentation columns with four-column tab stops.""" + columns = 0 + for char in line: + if char == " ": + columns += 1 + elif char == "\t": + columns += 4 - (columns % 4) + else: + break + return columns + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -546,22 +602,53 @@ def observe_entry_orientation(root, entry): "frontmatter_title": evidence_value("unavailable"), "provider_rendered_title": evidence_value("unavailable"), } - lines = text.removeprefix("\ufeff").splitlines() - in_fence = False - literal_h1 = False - for line in lines: - stripped = line.lstrip() - if stripped.startswith(("```", "~~~")): - in_fence = not in_fence - continue - if not in_fence and re.match(r"^#(?:\s+|$)", stripped): - literal_h1 = True - break + body_lines = _markdown_body_lines(text) + literal_h1 = None + if body_lines is not None: + fence = None + in_html_comment = False + in_mdx_comment = False + literal_h1 = False + for line in body_lines: + marker = _fence_marker(line) + if fence is not None: + if ( + marker is not None + and marker[0] == fence[0] + and marker[1] >= fence[1] + and not marker[2].strip() + ): + fence = None + continue + indented_code = _leading_indent_columns(line) >= 4 + if indented_code and not (in_html_comment or in_mdx_comment): + continue + stripped, in_html_comment, in_mdx_comment = _after_leading_comments( + line, in_html_comment, in_mdx_comment + ) + if not stripped: + continue + if marker is not None: + if marker[0] != "`" or "`" not in marker[2]: + fence = (marker[0], marker[1]) + continue + if re.match(r"^#(?:\s+|$)", stripped): + literal_h1 = True + break metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { - "literal_h1": evidence_value("completed", literal_h1), - "frontmatter_title": evidence_value("completed", isinstance(title, str) and bool(title.strip())), + "literal_h1": evidence_value("completed", literal_h1) + if literal_h1 is not None + else evidence_value("unavailable"), + "frontmatter_title": frontmatter_title, "provider_rendered_title": evidence_value("unavailable"), } diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index e3e1de8..5693765 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -366,6 +366,102 @@ def test_orientation_is_inert_and_does_not_change_rubric(self): {"entry": 20, "path_safety": 15, "links": 20, "anchors": 10, "reachability": 25, "titles": 10}, ) + def test_orientation_skips_heading_shaped_frontmatter(self): + scenarios = { + "comment": "---\n# not a Markdown H1\ntitle: Guide\n---\n\n## Start\n", + "block-scalar": "---\ntitle: |\n ---\n # not a Markdown H1\n---\n\n## Start\n", + "body-h1": "---\n# still frontmatter\ntitle: Guide\n---\n\n# Actual H1\n", + } + with tempfile.TemporaryDirectory() as td: + root = Path(td) + for name, source in scenarios.items(): + entry = root / f"{name}.md" + entry.write_text(source, encoding="utf-8") + observed = evidence.observe_entry_orientation(root, entry.name) + with self.subTest(name=name): + self.assertEqual( + observed["literal_h1"], + {"status": "completed", "value": name == "body-h1"}, + ) + self.assertEqual( + observed["frontmatter_title"]["status"], + "unavailable" if name == "block-scalar" else "completed", + ) + + unresolved = root / "unresolved.md" + unresolved.write_text("---\ntitle: Guide\n# unresolved frontmatter\n", encoding="utf-8") + observed = evidence.observe_entry_orientation(root, unresolved.name) + self.assertEqual(observed["literal_h1"], {"status": "unavailable", "value": None}) + self.assertEqual( + observed["frontmatter_title"], {"status": "unavailable", "value": None} + ) + + oversized = root / "oversized.md" + oversized.write_text( + "---\ntitle: Guide\n" + "x" * evidence.MAX_FRONTMATTER_BYTES, + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, oversized.name) + self.assertEqual(observed["literal_h1"], {"status": "unavailable", "value": None}) + self.assertEqual( + observed["frontmatter_title"], {"status": "unavailable", "value": None} + ) + + mixed = root / "mixed.md" + mixed.write_text( + "---\ntitle: Proven title\ntags: [one, two]\n---\n\n## Start\n", + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, mixed.name) + self.assertEqual( + observed["frontmatter_title"], {"status": "completed", "value": True} + ) + + duplicate = root / "duplicate.md" + duplicate.write_text( + "---\ntitle: First\ntitle: Second\n---\n\n## Start\n", + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, duplicate.name) + self.assertEqual( + observed["frontmatter_title"], {"status": "unavailable", "value": None} + ) + + def test_orientation_ignores_comments_and_indented_code(self): + scenarios = { + "html-comment": "\n## Start\n", + "mdx-comment": "{/*\n# not a heading\n*/}\n## Start\n", + "space-code": " # not a heading\n## Start\n", + "tab-code": "\t# not a heading\n## Start\n", + "mixed-tab-code": " \t# not a heading\n## Start\n", + "html-inline": " # not a heading\n", + "html-close": " # not a heading\n", + "mdx-inline": "{/* note */} # not a heading\n", + "four-backticks": "````\n```\n# not a heading\n````\n", + "mixed-fence": "```\n~~~\n# not a heading\n```\n", + "actual-h1": "# Actual H1\n", + "indented-html-opener": " " not in current, in_mdx_comment - if in_mdx_comment: - return "", in_html_comment, "*/}" not in current - if current.startswith("" not in current[4:], in_mdx_comment - if current.startswith("{/*"): - return "", in_html_comment, "*/}" not in current[3:] - return current, in_html_comment, in_mdx_comment +def _comment_remains_open(line, opening, closing): + start = line.find(opening) + return start >= 0 and line.find(closing, start + len(opening)) < 0 + + +def _toggle_unescaped_backticks(line, open_state): + escaped = False + for char in line: + if char == "\\": + escaped = not escaped + continue + if char == "`" and not escaped: + open_state = not open_state + escaped = False + return open_state def _fence_marker(line): @@ -605,11 +608,33 @@ def observe_entry_orientation(root, entry): body_lines = _markdown_body_lines(text) literal_h1 = None if body_lines is not None: + component_document = Path(relative).suffix.casefold() == ".mdx" fence = None in_html_comment = False in_mdx_comment = False + raw_html_tag = None + in_mdx_esm = False + mdx_esm_template = False + uncertain = False literal_h1 = False for line in body_lines: + if in_html_comment: + if "-->" in line: + in_html_comment = False + continue + if in_mdx_comment: + if "*/}" in line: + in_mdx_comment = False + continue + if raw_html_tag is not None: + if re.search(rf"", line, re.IGNORECASE): + raw_html_tag = None + continue + if in_mdx_esm: + mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) + if not line.strip() and not mdx_esm_template: + in_mdx_esm = False + continue marker = _fence_marker(line) if fence is not None: if ( @@ -621,11 +646,9 @@ def observe_entry_orientation(root, entry): fence = None continue indented_code = _leading_indent_columns(line) >= 4 - if indented_code and not (in_html_comment or in_mdx_comment): + if indented_code: continue - stripped, in_html_comment, in_mdx_comment = _after_leading_comments( - line, in_html_comment, in_mdx_comment - ) + stripped = line.lstrip() if not stripped: continue if marker is not None: @@ -635,6 +658,27 @@ def observe_entry_orientation(root, entry): if re.match(r"^#(?:\s+|$)", stripped): literal_h1 = True break + if "") + continue + if component_document and "{/*" in line: + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + continue + raw_match = re.match( + r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + ) + if raw_match is not None: + tag = raw_match.group(1) + if re.search(rf"", stripped, re.IGNORECASE) is None: + raw_html_tag = tag + continue + if component_document and re.match(r"^(?:import|export)(?:\s|\{|$)", stripped): + uncertain = True + in_mdx_esm = True + mdx_esm_template = _toggle_unescaped_backticks(line, False) + continue + if literal_h1 is False and (uncertain or in_mdx_esm): + literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/formats.py b/adapters/claude/skills/docs/scripts/_docs_checker/formats.py index 4223eee..d83fff6 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/formats.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/formats.py @@ -83,7 +83,10 @@ def parse_frontmatter_scalars(text): unresolved.append(key) continue value = decoded - elif value.startswith("'") and value.endswith("'") and len(value) >= 2: + elif value.startswith("'"): + if not value.endswith("'") or len(value) < 2: + unresolved.append(key) + continue value = value[1:-1].replace("''", "'") elif value.casefold() in {"true", "false"}: value = value.casefold() == "true" diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 4ead0b2..bcb1ac2 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -545,18 +545,21 @@ def _markdown_body_lines(text): return None -def _after_leading_comments(line, in_html_comment, in_mdx_comment): - """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" - current = line.lstrip() - if in_html_comment: - return "", "-->" not in current, in_mdx_comment - if in_mdx_comment: - return "", in_html_comment, "*/}" not in current - if current.startswith("" not in current[4:], in_mdx_comment - if current.startswith("{/*"): - return "", in_html_comment, "*/}" not in current[3:] - return current, in_html_comment, in_mdx_comment +def _comment_remains_open(line, opening, closing): + start = line.find(opening) + return start >= 0 and line.find(closing, start + len(opening)) < 0 + + +def _toggle_unescaped_backticks(line, open_state): + escaped = False + for char in line: + if char == "\\": + escaped = not escaped + continue + if char == "`" and not escaped: + open_state = not open_state + escaped = False + return open_state def _fence_marker(line): @@ -605,11 +608,33 @@ def observe_entry_orientation(root, entry): body_lines = _markdown_body_lines(text) literal_h1 = None if body_lines is not None: + component_document = Path(relative).suffix.casefold() == ".mdx" fence = None in_html_comment = False in_mdx_comment = False + raw_html_tag = None + in_mdx_esm = False + mdx_esm_template = False + uncertain = False literal_h1 = False for line in body_lines: + if in_html_comment: + if "-->" in line: + in_html_comment = False + continue + if in_mdx_comment: + if "*/}" in line: + in_mdx_comment = False + continue + if raw_html_tag is not None: + if re.search(rf"", line, re.IGNORECASE): + raw_html_tag = None + continue + if in_mdx_esm: + mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) + if not line.strip() and not mdx_esm_template: + in_mdx_esm = False + continue marker = _fence_marker(line) if fence is not None: if ( @@ -621,11 +646,9 @@ def observe_entry_orientation(root, entry): fence = None continue indented_code = _leading_indent_columns(line) >= 4 - if indented_code and not (in_html_comment or in_mdx_comment): + if indented_code: continue - stripped, in_html_comment, in_mdx_comment = _after_leading_comments( - line, in_html_comment, in_mdx_comment - ) + stripped = line.lstrip() if not stripped: continue if marker is not None: @@ -635,6 +658,27 @@ def observe_entry_orientation(root, entry): if re.match(r"^#(?:\s+|$)", stripped): literal_h1 = True break + if "") + continue + if component_document and "{/*" in line: + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + continue + raw_match = re.match( + r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + ) + if raw_match is not None: + tag = raw_match.group(1) + if re.search(rf"", stripped, re.IGNORECASE) is None: + raw_html_tag = tag + continue + if component_document and re.match(r"^(?:import|export)(?:\s|\{|$)", stripped): + uncertain = True + in_mdx_esm = True + mdx_esm_template = _toggle_unescaped_backticks(line, False) + continue + if literal_h1 is False and (uncertain or in_mdx_esm): + literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) diff --git a/adapters/copilot/scripts/_docs_checker/formats.py b/adapters/copilot/scripts/_docs_checker/formats.py index 4223eee..d83fff6 100644 --- a/adapters/copilot/scripts/_docs_checker/formats.py +++ b/adapters/copilot/scripts/_docs_checker/formats.py @@ -83,7 +83,10 @@ def parse_frontmatter_scalars(text): unresolved.append(key) continue value = decoded - elif value.startswith("'") and value.endswith("'") and len(value) >= 2: + elif value.startswith("'"): + if not value.endswith("'") or len(value) < 2: + unresolved.append(key) + continue value = value[1:-1].replace("''", "'") elif value.casefold() in {"true", "false"}: value = value.casefold() == "true" diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 4ead0b2..bcb1ac2 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -545,18 +545,21 @@ def _markdown_body_lines(text): return None -def _after_leading_comments(line, in_html_comment, in_mdx_comment): - """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" - current = line.lstrip() - if in_html_comment: - return "", "-->" not in current, in_mdx_comment - if in_mdx_comment: - return "", in_html_comment, "*/}" not in current - if current.startswith("" not in current[4:], in_mdx_comment - if current.startswith("{/*"): - return "", in_html_comment, "*/}" not in current[3:] - return current, in_html_comment, in_mdx_comment +def _comment_remains_open(line, opening, closing): + start = line.find(opening) + return start >= 0 and line.find(closing, start + len(opening)) < 0 + + +def _toggle_unescaped_backticks(line, open_state): + escaped = False + for char in line: + if char == "\\": + escaped = not escaped + continue + if char == "`" and not escaped: + open_state = not open_state + escaped = False + return open_state def _fence_marker(line): @@ -605,11 +608,33 @@ def observe_entry_orientation(root, entry): body_lines = _markdown_body_lines(text) literal_h1 = None if body_lines is not None: + component_document = Path(relative).suffix.casefold() == ".mdx" fence = None in_html_comment = False in_mdx_comment = False + raw_html_tag = None + in_mdx_esm = False + mdx_esm_template = False + uncertain = False literal_h1 = False for line in body_lines: + if in_html_comment: + if "-->" in line: + in_html_comment = False + continue + if in_mdx_comment: + if "*/}" in line: + in_mdx_comment = False + continue + if raw_html_tag is not None: + if re.search(rf"", line, re.IGNORECASE): + raw_html_tag = None + continue + if in_mdx_esm: + mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) + if not line.strip() and not mdx_esm_template: + in_mdx_esm = False + continue marker = _fence_marker(line) if fence is not None: if ( @@ -621,11 +646,9 @@ def observe_entry_orientation(root, entry): fence = None continue indented_code = _leading_indent_columns(line) >= 4 - if indented_code and not (in_html_comment or in_mdx_comment): + if indented_code: continue - stripped, in_html_comment, in_mdx_comment = _after_leading_comments( - line, in_html_comment, in_mdx_comment - ) + stripped = line.lstrip() if not stripped: continue if marker is not None: @@ -635,6 +658,27 @@ def observe_entry_orientation(root, entry): if re.match(r"^#(?:\s+|$)", stripped): literal_h1 = True break + if "") + continue + if component_document and "{/*" in line: + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + continue + raw_match = re.match( + r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + ) + if raw_match is not None: + tag = raw_match.group(1) + if re.search(rf"", stripped, re.IGNORECASE) is None: + raw_html_tag = tag + continue + if component_document and re.match(r"^(?:import|export)(?:\s|\{|$)", stripped): + uncertain = True + in_mdx_esm = True + mdx_esm_template = _toggle_unescaped_backticks(line, False) + continue + if literal_h1 is False and (uncertain or in_mdx_esm): + literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) diff --git a/adapters/cursor/scripts/_docs_checker/formats.py b/adapters/cursor/scripts/_docs_checker/formats.py index 4223eee..d83fff6 100644 --- a/adapters/cursor/scripts/_docs_checker/formats.py +++ b/adapters/cursor/scripts/_docs_checker/formats.py @@ -83,7 +83,10 @@ def parse_frontmatter_scalars(text): unresolved.append(key) continue value = decoded - elif value.startswith("'") and value.endswith("'") and len(value) >= 2: + elif value.startswith("'"): + if not value.endswith("'") or len(value) < 2: + unresolved.append(key) + continue value = value[1:-1].replace("''", "'") elif value.casefold() in {"true", "false"}: value = value.casefold() == "true" diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 4ead0b2..bcb1ac2 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -545,18 +545,21 @@ def _markdown_body_lines(text): return None -def _after_leading_comments(line, in_html_comment, in_mdx_comment): - """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" - current = line.lstrip() - if in_html_comment: - return "", "-->" not in current, in_mdx_comment - if in_mdx_comment: - return "", in_html_comment, "*/}" not in current - if current.startswith("" not in current[4:], in_mdx_comment - if current.startswith("{/*"): - return "", in_html_comment, "*/}" not in current[3:] - return current, in_html_comment, in_mdx_comment +def _comment_remains_open(line, opening, closing): + start = line.find(opening) + return start >= 0 and line.find(closing, start + len(opening)) < 0 + + +def _toggle_unescaped_backticks(line, open_state): + escaped = False + for char in line: + if char == "\\": + escaped = not escaped + continue + if char == "`" and not escaped: + open_state = not open_state + escaped = False + return open_state def _fence_marker(line): @@ -605,11 +608,33 @@ def observe_entry_orientation(root, entry): body_lines = _markdown_body_lines(text) literal_h1 = None if body_lines is not None: + component_document = Path(relative).suffix.casefold() == ".mdx" fence = None in_html_comment = False in_mdx_comment = False + raw_html_tag = None + in_mdx_esm = False + mdx_esm_template = False + uncertain = False literal_h1 = False for line in body_lines: + if in_html_comment: + if "-->" in line: + in_html_comment = False + continue + if in_mdx_comment: + if "*/}" in line: + in_mdx_comment = False + continue + if raw_html_tag is not None: + if re.search(rf"", line, re.IGNORECASE): + raw_html_tag = None + continue + if in_mdx_esm: + mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) + if not line.strip() and not mdx_esm_template: + in_mdx_esm = False + continue marker = _fence_marker(line) if fence is not None: if ( @@ -621,11 +646,9 @@ def observe_entry_orientation(root, entry): fence = None continue indented_code = _leading_indent_columns(line) >= 4 - if indented_code and not (in_html_comment or in_mdx_comment): + if indented_code: continue - stripped, in_html_comment, in_mdx_comment = _after_leading_comments( - line, in_html_comment, in_mdx_comment - ) + stripped = line.lstrip() if not stripped: continue if marker is not None: @@ -635,6 +658,27 @@ def observe_entry_orientation(root, entry): if re.match(r"^#(?:\s+|$)", stripped): literal_h1 = True break + if "") + continue + if component_document and "{/*" in line: + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + continue + raw_match = re.match( + r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + ) + if raw_match is not None: + tag = raw_match.group(1) + if re.search(rf"", stripped, re.IGNORECASE) is None: + raw_html_tag = tag + continue + if component_document and re.match(r"^(?:import|export)(?:\s|\{|$)", stripped): + uncertain = True + in_mdx_esm = True + mdx_esm_template = _toggle_unescaped_backticks(line, False) + continue + if literal_h1 is False and (uncertain or in_mdx_esm): + literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) diff --git a/adapters/grok/scripts/_docs_checker/formats.py b/adapters/grok/scripts/_docs_checker/formats.py index 4223eee..d83fff6 100644 --- a/adapters/grok/scripts/_docs_checker/formats.py +++ b/adapters/grok/scripts/_docs_checker/formats.py @@ -83,7 +83,10 @@ def parse_frontmatter_scalars(text): unresolved.append(key) continue value = decoded - elif value.startswith("'") and value.endswith("'") and len(value) >= 2: + elif value.startswith("'"): + if not value.endswith("'") or len(value) < 2: + unresolved.append(key) + continue value = value[1:-1].replace("''", "'") elif value.casefold() in {"true", "false"}: value = value.casefold() == "true" diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 4ead0b2..bcb1ac2 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -545,18 +545,21 @@ def _markdown_body_lines(text): return None -def _after_leading_comments(line, in_html_comment, in_mdx_comment): - """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" - current = line.lstrip() - if in_html_comment: - return "", "-->" not in current, in_mdx_comment - if in_mdx_comment: - return "", in_html_comment, "*/}" not in current - if current.startswith("" not in current[4:], in_mdx_comment - if current.startswith("{/*"): - return "", in_html_comment, "*/}" not in current[3:] - return current, in_html_comment, in_mdx_comment +def _comment_remains_open(line, opening, closing): + start = line.find(opening) + return start >= 0 and line.find(closing, start + len(opening)) < 0 + + +def _toggle_unescaped_backticks(line, open_state): + escaped = False + for char in line: + if char == "\\": + escaped = not escaped + continue + if char == "`" and not escaped: + open_state = not open_state + escaped = False + return open_state def _fence_marker(line): @@ -605,11 +608,33 @@ def observe_entry_orientation(root, entry): body_lines = _markdown_body_lines(text) literal_h1 = None if body_lines is not None: + component_document = Path(relative).suffix.casefold() == ".mdx" fence = None in_html_comment = False in_mdx_comment = False + raw_html_tag = None + in_mdx_esm = False + mdx_esm_template = False + uncertain = False literal_h1 = False for line in body_lines: + if in_html_comment: + if "-->" in line: + in_html_comment = False + continue + if in_mdx_comment: + if "*/}" in line: + in_mdx_comment = False + continue + if raw_html_tag is not None: + if re.search(rf"", line, re.IGNORECASE): + raw_html_tag = None + continue + if in_mdx_esm: + mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) + if not line.strip() and not mdx_esm_template: + in_mdx_esm = False + continue marker = _fence_marker(line) if fence is not None: if ( @@ -621,11 +646,9 @@ def observe_entry_orientation(root, entry): fence = None continue indented_code = _leading_indent_columns(line) >= 4 - if indented_code and not (in_html_comment or in_mdx_comment): + if indented_code: continue - stripped, in_html_comment, in_mdx_comment = _after_leading_comments( - line, in_html_comment, in_mdx_comment - ) + stripped = line.lstrip() if not stripped: continue if marker is not None: @@ -635,6 +658,27 @@ def observe_entry_orientation(root, entry): if re.match(r"^#(?:\s+|$)", stripped): literal_h1 = True break + if "") + continue + if component_document and "{/*" in line: + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + continue + raw_match = re.match( + r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + ) + if raw_match is not None: + tag = raw_match.group(1) + if re.search(rf"", stripped, re.IGNORECASE) is None: + raw_html_tag = tag + continue + if component_document and re.match(r"^(?:import|export)(?:\s|\{|$)", stripped): + uncertain = True + in_mdx_esm = True + mdx_esm_template = _toggle_unescaped_backticks(line, False) + continue + if literal_h1 is False and (uncertain or in_mdx_esm): + literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py b/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py index 4223eee..d83fff6 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py @@ -83,7 +83,10 @@ def parse_frontmatter_scalars(text): unresolved.append(key) continue value = decoded - elif value.startswith("'") and value.endswith("'") and len(value) >= 2: + elif value.startswith("'"): + if not value.endswith("'") or len(value) < 2: + unresolved.append(key) + continue value = value[1:-1].replace("''", "'") elif value.casefold() in {"true", "false"}: value = value.casefold() == "true" diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 4ead0b2..bcb1ac2 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -545,18 +545,21 @@ def _markdown_body_lines(text): return None -def _after_leading_comments(line, in_html_comment, in_mdx_comment): - """Skip lines occupied by leading HTML/MDX comments and preserve their state.""" - current = line.lstrip() - if in_html_comment: - return "", "-->" not in current, in_mdx_comment - if in_mdx_comment: - return "", in_html_comment, "*/}" not in current - if current.startswith("" not in current[4:], in_mdx_comment - if current.startswith("{/*"): - return "", in_html_comment, "*/}" not in current[3:] - return current, in_html_comment, in_mdx_comment +def _comment_remains_open(line, opening, closing): + start = line.find(opening) + return start >= 0 and line.find(closing, start + len(opening)) < 0 + + +def _toggle_unescaped_backticks(line, open_state): + escaped = False + for char in line: + if char == "\\": + escaped = not escaped + continue + if char == "`" and not escaped: + open_state = not open_state + escaped = False + return open_state def _fence_marker(line): @@ -605,11 +608,33 @@ def observe_entry_orientation(root, entry): body_lines = _markdown_body_lines(text) literal_h1 = None if body_lines is not None: + component_document = Path(relative).suffix.casefold() == ".mdx" fence = None in_html_comment = False in_mdx_comment = False + raw_html_tag = None + in_mdx_esm = False + mdx_esm_template = False + uncertain = False literal_h1 = False for line in body_lines: + if in_html_comment: + if "-->" in line: + in_html_comment = False + continue + if in_mdx_comment: + if "*/}" in line: + in_mdx_comment = False + continue + if raw_html_tag is not None: + if re.search(rf"", line, re.IGNORECASE): + raw_html_tag = None + continue + if in_mdx_esm: + mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) + if not line.strip() and not mdx_esm_template: + in_mdx_esm = False + continue marker = _fence_marker(line) if fence is not None: if ( @@ -621,11 +646,9 @@ def observe_entry_orientation(root, entry): fence = None continue indented_code = _leading_indent_columns(line) >= 4 - if indented_code and not (in_html_comment or in_mdx_comment): + if indented_code: continue - stripped, in_html_comment, in_mdx_comment = _after_leading_comments( - line, in_html_comment, in_mdx_comment - ) + stripped = line.lstrip() if not stripped: continue if marker is not None: @@ -635,6 +658,27 @@ def observe_entry_orientation(root, entry): if re.match(r"^#(?:\s+|$)", stripped): literal_h1 = True break + if "") + continue + if component_document and "{/*" in line: + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + continue + raw_match = re.match( + r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + ) + if raw_match is not None: + tag = raw_match.group(1) + if re.search(rf"", stripped, re.IGNORECASE) is None: + raw_html_tag = tag + continue + if component_document and re.match(r"^(?:import|export)(?:\s|\{|$)", stripped): + uncertain = True + in_mdx_esm = True + mdx_esm_template = _toggle_unescaped_backticks(line, False) + continue + if literal_h1 is False and (uncertain or in_mdx_esm): + literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) diff --git a/skills/docs/scripts/_docs_checker/formats.py b/skills/docs/scripts/_docs_checker/formats.py index 4223eee..d83fff6 100644 --- a/skills/docs/scripts/_docs_checker/formats.py +++ b/skills/docs/scripts/_docs_checker/formats.py @@ -83,7 +83,10 @@ def parse_frontmatter_scalars(text): unresolved.append(key) continue value = decoded - elif value.startswith("'") and value.endswith("'") and len(value) >= 2: + elif value.startswith("'"): + if not value.endswith("'") or len(value) < 2: + unresolved.append(key) + continue value = value[1:-1].replace("''", "'") elif value.casefold() in {"true", "false"}: value = value.casefold() == "true" diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 5693765..3aa93af 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -427,6 +427,16 @@ def test_orientation_skips_heading_shaped_frontmatter(self): observed["frontmatter_title"], {"status": "unavailable", "value": None} ) + malformed_quote = root / "malformed-quote.md" + malformed_quote.write_text( + "---\ntitle: 'Guide\n---\n\n## Start\n", + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, malformed_quote.name) + self.assertEqual( + observed["frontmatter_title"], {"status": "unavailable", "value": None} + ) + def test_orientation_ignores_comments_and_indented_code(self): scenarios = { "html-comment": "\n## Start\n", @@ -462,6 +472,46 @@ def test_orientation_ignores_comments_and_indented_code(self): {"status": "completed", "value": name in expected_h1}, ) + def test_orientation_respects_markdown_and_mdx_contexts(self): + scenarios = { + "plain-md-mdx-syntax.md": ( + "{/*\n# Actual H1\n*/}\n", + {"status": "completed", "value": True}, + ), + "midline-html.mdx": ( + "prefix \n## Start\n", + {"status": "completed", "value": False}, + ), + "midline-mdx.mdx": ( + "prefix {/*\n# not a heading\n*/}\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-pre.md": ( + "
\n# not a heading\n
\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-script.mdx": ( + "\n## Start\n", + {"status": "completed", "value": False}, + ), + "esm-template.mdx": ( + "export const example = `\n# not a heading\n`\n", + {"status": "unavailable", "value": None}, + ), + "esm-then-heading.mdx": ( + "import Example from './example'\n\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + } + with tempfile.TemporaryDirectory() as td: + root = Path(td) + for name, (source, expected) in scenarios.items(): + entry = root / name + entry.write_text(source, encoding="utf-8") + observed = evidence.observe_entry_orientation(root, entry.name) + with self.subTest(name=name): + self.assertEqual(observed["literal_h1"], expected) + def test_stdout_receipt_entrypoint_combines_one_checker_run(self): with tempfile.TemporaryDirectory() as td: base = Path(td) From 72c8c65d792a3cbdabb302a31c6c9942160ebfad Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 05:41:01 -0400 Subject: [PATCH 07/20] Close final 0.1.4 review gaps --- .../docs/scripts/_docs_checker/evidence.py | 471 ++++++++++++++++-- .../skills/docs/scripts/evidence_receipt.py | 9 +- .../copilot/scripts/_docs_checker/evidence.py | 471 ++++++++++++++++-- adapters/copilot/scripts/evidence_receipt.py | 9 +- .../cursor/scripts/_docs_checker/evidence.py | 471 ++++++++++++++++-- adapters/cursor/scripts/evidence_receipt.py | 9 +- .../grok/scripts/_docs_checker/evidence.py | 471 ++++++++++++++++-- adapters/grok/scripts/evidence_receipt.py | 9 +- .../docs/scripts/_docs_checker/evidence.py | 471 ++++++++++++++++-- .../skills/docs/scripts/evidence_receipt.py | 9 +- evals/docs-corpus-baseline-v1.json | 15 +- skills/docs/scripts/_docs_checker/evidence.py | 471 ++++++++++++++++-- skills/docs/scripts/evidence_receipt.py | 9 +- tests/test_docs_evidence.py | 417 +++++++++++++++- 14 files changed, 3011 insertions(+), 301 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index bcb1ac2..2c0fb67 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -39,6 +39,7 @@ ) _NETWORK_PATH = re.compile(r"(?= 0 and line.find(closing, start + len(opening)) < 0 +def _comment_remains_open(line, opening, closing, already_open=False): + position = 0 + if already_open: + end = line.find(closing) + if end < 0: + return True + position = end + len(closing) + while True: + start = _find_unescaped(line, opening, position) + if start < 0: + return False + if opening == "", start): + position = start + len("") + continue + if ( + start > len(line) - len(line.lstrip(" \t")) + and line.startswith("", start) + ): + position = start + len("") + continue + end = line.find(closing, start + len(opening)) + if end < 0: + return True + position = end + len(closing) -def _toggle_unescaped_backticks(line, open_state): +def _comment_close_end(line, start, opening, closing): + if opening == "", start): + return start + len("") + if start > len(line) - len(line.lstrip(" \t")) and line.startswith("", start): + return start + len("") + end = line.find(closing, start + len(opening)) + return -1 if end < 0 else end + len(closing) + + +def _tag_close_end(line, start): + quote = None escaped = False - for char in line: - if char == "\\": - escaped = not escaped + for index in range(start, len(line)): + char = line[index] + if quote is not None: + if char == "\\" and not escaped: + escaped = True + continue + if char == quote and not escaped: + quote = None + escaped = False continue - if char == "`" and not escaped: - open_state = not open_state - escaped = False - return open_state + if char in {'"', "'"}: + quote = char + elif char == "{": + return -1 + elif char == ">": + return index + 1 + return -1 + + +def _find_unescaped(value, token, start=0): + position = start + while True: + position = value.find(token, position) + if position < 0: + return -1 + slashes = 0 + cursor = position - 1 + while cursor >= 0 and value[cursor] == "\\": + slashes += 1 + cursor -= 1 + if slashes % 2 == 0: + return position + position += len(token) + + +def _find_tick_run(value, length, start=0): + marker = "`" * length + position = start + while True: + position = value.find(marker, position) + if position < 0: + return -1 + before = position > 0 and value[position - 1] == "`" + after_position = position + length + after = after_position < len(value) and value[after_position] == "`" + if not before and not after: + return position + position = after_position + + +def _without_inline_code_spans(line, open_length=0): + """Mask CommonMark backtick spans on one line and return multiline state.""" + masked = list(line) + index = 0 + if open_length: + close = _find_tick_run(line, open_length) + if close < 0: + for position, char in enumerate(masked): + if char not in "\r\n": + masked[position] = "x" + return "".join(masked), open_length + for position in range(close + open_length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + open_length + while index < len(line): + start = _find_unescaped(line, "`", index) + if start < 0: + break + index = start + if line[index] != "`": + index += 1 + continue + while index < len(line) and line[index] == "`": + index += 1 + length = index - start + close = _find_tick_run(line, length, index) + if close < 0: + for position in range(start, len(masked)): + if masked[position] not in "\r\n": + masked[position] = "x" + return "".join(masked), length + for position in range(start, close + length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + length + return "".join(masked), 0 + + +def _is_single_line_mdx_esm(line): + stripped = line.strip() + simple_import = re.fullmatch( + r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + stripped, + ) + simple_export = re.fullmatch( + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + stripped, + ) + return simple_import is not None or simple_export is not None + + +_RAW_HTML_BLANK_TAGS = frozenset( + { + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", + "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", + "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", + "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", + "track", "ul", + } +) def _fence_marker(line): @@ -584,6 +735,10 @@ def _leading_indent_columns(line): return columns +def _is_ascii_blank_line(line): + return not line.rstrip("\r\n").strip(" \t") + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -598,7 +753,8 @@ def observe_entry_orientation(root, entry): try: if path.stat().st_size > 2 * 1024 * 1024: raise ValueError("entry exceeds capacity") - text = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as stream: + text = stream.read() except (OSError, UnicodeError, ValueError): return { "literal_h1": evidence_value("unavailable"), @@ -610,74 +766,293 @@ def observe_entry_orientation(root, entry): if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" fence = None + inline_code_length = 0 in_html_comment = False + html_comment_block = False in_mdx_comment = False raw_html_tag = None + raw_html_terminator = None + raw_html_until_blank = False in_mdx_esm = False - mdx_esm_template = False + in_mdx_expression = False uncertain = False literal_h1 = False - for line in body_lines: + for raw_line in body_lines: + line = raw_line.rstrip("\r\n") if in_html_comment: - if "-->" in line: + if not html_comment_block and ( + _is_ascii_blank_line(line) + or ( + _leading_indent_columns(line) < 4 + and re.match( + r"^#(?:[ \t]+|$)", line.lstrip(" \t") + ) + ) + ): in_html_comment = False - continue + html_comment_block = False + if _is_ascii_blank_line(line): + continue + else: + in_html_comment = _comment_remains_open( + line, "", True + ) + if not in_html_comment: + html_comment_block = False + continue if in_mdx_comment: - if "*/}" in line: - in_mdx_comment = False + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}", True) continue if raw_html_tag is not None: - if re.search(rf"", line, re.IGNORECASE): + if re.search( + r"", line, re.IGNORECASE + ): raw_html_tag = None continue - if in_mdx_esm: - mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) - if not line.strip() and not mdx_esm_template: - in_mdx_esm = False + if raw_html_terminator is not None: + if raw_html_terminator in line: + raw_html_terminator = None + continue + if raw_html_until_blank: + if _is_ascii_blank_line(line): + raw_html_until_blank = False + continue + if in_mdx_esm or in_mdx_expression: continue + list_item = re.match( + r"^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(.*)$", + line.rstrip("\r\n"), + ) + if list_item is not None: + nested = list_item.group(1) + nested_fence = _fence_marker(nested) + nested_raw = re.match( + r"^<(?:pre|script|style|textarea)(?:[ \t]|>|$)", + nested, + re.IGNORECASE, + ) or re.match( + r"^|$)", nested + ) + if ( + nested_fence is not None + and ( + nested_fence[0] != "`" or "`" not in nested_fence[2] + ) + ) or nested_raw is not None: + uncertain = True + break marker = _fence_marker(line) if fence is not None: if ( marker is not None and marker[0] == fence[0] and marker[1] >= fence[1] - and not marker[2].strip() + and not marker[2].strip(" \t") ): fence = None continue - indented_code = _leading_indent_columns(line) >= 4 - if indented_code: - continue - stripped = line.lstrip() - if not stripped: - continue - if marker is not None: + if inline_code_length: + interrupt = line.lstrip(" \t") + interrupt_marker = _fence_marker(line) + if _is_ascii_blank_line(line): + inline_code_length = 0 + continue + if ( + re.match(r"^#(?:[ \t]+|$)", interrupt) + or ( + interrupt_marker is not None + and ( + interrupt_marker[0] != "`" + or "`" not in interrupt_marker[2] + ) + ) + or re.match(r"^") + if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue - if component_document and "{/*" in line: - in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + pre_stripped = line.lstrip(" \t") + if component_document and re.match( + r"^(?:import|export)(?:\s|\{|$)", pre_stripped + ): + uncertain = True + in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + continue + jsx_match = re.match( + r"^|$)", pre_stripped + ) + if component_document and jsx_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True continue raw_match = re.match( - r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + r"^<(pre|script|style|textarea)(?:[ \t]|>|$)", + pre_stripped, + re.IGNORECASE, ) if raw_match is not None: - tag = raw_match.group(1) - if re.search(rf"", stripped, re.IGNORECASE) is None: - raw_html_tag = tag + if re.search( + r"", + pre_stripped, + re.IGNORECASE, + ) is None: + raw_html_tag = raw_match.group(1) + continue + if pre_stripped.startswith("" not in pre_stripped[2:]: + raw_html_terminator = "?>" + continue + if pre_stripped.startswith("" not in pre_stripped[9:]: + raw_html_terminator = "]]>" + continue + if re.match(r"^" not in pre_stripped[2:]: + raw_html_terminator = ">" + continue + block_match = re.match( + r"^|$)", pre_stripped + ) + if ( + block_match is not None + and block_match.group(1).casefold() in _RAW_HTML_BLANK_TAGS + and not ( + component_document and block_match.group(1)[0].isupper() + ) + ): + raw_html_until_blank = True + continue + if component_document and block_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True + continue + if not component_document and block_match is not None: + uncertain = True + raw_html_until_blank = True + continue + leading = len(line) - len(line.lstrip(" \t")) + html_comment = _find_unescaped(line, "") + html_comment_block = in_html_comment + comment_end = _comment_close_end(line, html_comment, "") + remainder = line[comment_end:] if comment_end >= 0 else "" + if ( + not in_html_comment + and component_document + and ( + _find_unescaped(remainder, "{") >= 0 + or re.search(r"= 0 else "" + if ( + not in_mdx_comment + and ( + _find_unescaped(remainder, "{") >= 0 + or _find_unescaped(remainder, "") + if end < 0: + in_html_comment = True + html_comment_block = False + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + if kind == "mdx-comment": + end = _comment_close_end(scan_line, position, "{/*", "*/}") + if end < 0: + in_mdx_comment = True + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + uncertain = True + in_mdx_expression = True + break + if literal_h1 is False and ( + uncertain or in_mdx_esm or in_mdx_expression or inline_code_length + ): literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") diff --git a/adapters/claude/skills/docs/scripts/evidence_receipt.py b/adapters/claude/skills/docs/scripts/evidence_receipt.py index b79532a..555bd09 100644 --- a/adapters/claude/skills/docs/scripts/evidence_receipt.py +++ b/adapters/claude/skills/docs/scripts/evidence_receipt.py @@ -3,10 +3,14 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import subprocess -import sys from pathlib import Path from _docs_checker.evidence import ( @@ -19,6 +23,9 @@ from _docs_checker.init_adoption import SKILL_VERSION from check import check +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + _PARSER = argparse.ArgumentParser() _PARSER.add_argument("root") diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index bcb1ac2..2c0fb67 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -39,6 +39,7 @@ ) _NETWORK_PATH = re.compile(r"(?= 0 and line.find(closing, start + len(opening)) < 0 +def _comment_remains_open(line, opening, closing, already_open=False): + position = 0 + if already_open: + end = line.find(closing) + if end < 0: + return True + position = end + len(closing) + while True: + start = _find_unescaped(line, opening, position) + if start < 0: + return False + if opening == "", start): + position = start + len("") + continue + if ( + start > len(line) - len(line.lstrip(" \t")) + and line.startswith("", start) + ): + position = start + len("") + continue + end = line.find(closing, start + len(opening)) + if end < 0: + return True + position = end + len(closing) -def _toggle_unescaped_backticks(line, open_state): +def _comment_close_end(line, start, opening, closing): + if opening == "", start): + return start + len("") + if start > len(line) - len(line.lstrip(" \t")) and line.startswith("", start): + return start + len("") + end = line.find(closing, start + len(opening)) + return -1 if end < 0 else end + len(closing) + + +def _tag_close_end(line, start): + quote = None escaped = False - for char in line: - if char == "\\": - escaped = not escaped + for index in range(start, len(line)): + char = line[index] + if quote is not None: + if char == "\\" and not escaped: + escaped = True + continue + if char == quote and not escaped: + quote = None + escaped = False continue - if char == "`" and not escaped: - open_state = not open_state - escaped = False - return open_state + if char in {'"', "'"}: + quote = char + elif char == "{": + return -1 + elif char == ">": + return index + 1 + return -1 + + +def _find_unescaped(value, token, start=0): + position = start + while True: + position = value.find(token, position) + if position < 0: + return -1 + slashes = 0 + cursor = position - 1 + while cursor >= 0 and value[cursor] == "\\": + slashes += 1 + cursor -= 1 + if slashes % 2 == 0: + return position + position += len(token) + + +def _find_tick_run(value, length, start=0): + marker = "`" * length + position = start + while True: + position = value.find(marker, position) + if position < 0: + return -1 + before = position > 0 and value[position - 1] == "`" + after_position = position + length + after = after_position < len(value) and value[after_position] == "`" + if not before and not after: + return position + position = after_position + + +def _without_inline_code_spans(line, open_length=0): + """Mask CommonMark backtick spans on one line and return multiline state.""" + masked = list(line) + index = 0 + if open_length: + close = _find_tick_run(line, open_length) + if close < 0: + for position, char in enumerate(masked): + if char not in "\r\n": + masked[position] = "x" + return "".join(masked), open_length + for position in range(close + open_length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + open_length + while index < len(line): + start = _find_unescaped(line, "`", index) + if start < 0: + break + index = start + if line[index] != "`": + index += 1 + continue + while index < len(line) and line[index] == "`": + index += 1 + length = index - start + close = _find_tick_run(line, length, index) + if close < 0: + for position in range(start, len(masked)): + if masked[position] not in "\r\n": + masked[position] = "x" + return "".join(masked), length + for position in range(start, close + length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + length + return "".join(masked), 0 + + +def _is_single_line_mdx_esm(line): + stripped = line.strip() + simple_import = re.fullmatch( + r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + stripped, + ) + simple_export = re.fullmatch( + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + stripped, + ) + return simple_import is not None or simple_export is not None + + +_RAW_HTML_BLANK_TAGS = frozenset( + { + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", + "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", + "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", + "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", + "track", "ul", + } +) def _fence_marker(line): @@ -584,6 +735,10 @@ def _leading_indent_columns(line): return columns +def _is_ascii_blank_line(line): + return not line.rstrip("\r\n").strip(" \t") + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -598,7 +753,8 @@ def observe_entry_orientation(root, entry): try: if path.stat().st_size > 2 * 1024 * 1024: raise ValueError("entry exceeds capacity") - text = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as stream: + text = stream.read() except (OSError, UnicodeError, ValueError): return { "literal_h1": evidence_value("unavailable"), @@ -610,74 +766,293 @@ def observe_entry_orientation(root, entry): if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" fence = None + inline_code_length = 0 in_html_comment = False + html_comment_block = False in_mdx_comment = False raw_html_tag = None + raw_html_terminator = None + raw_html_until_blank = False in_mdx_esm = False - mdx_esm_template = False + in_mdx_expression = False uncertain = False literal_h1 = False - for line in body_lines: + for raw_line in body_lines: + line = raw_line.rstrip("\r\n") if in_html_comment: - if "-->" in line: + if not html_comment_block and ( + _is_ascii_blank_line(line) + or ( + _leading_indent_columns(line) < 4 + and re.match( + r"^#(?:[ \t]+|$)", line.lstrip(" \t") + ) + ) + ): in_html_comment = False - continue + html_comment_block = False + if _is_ascii_blank_line(line): + continue + else: + in_html_comment = _comment_remains_open( + line, "", True + ) + if not in_html_comment: + html_comment_block = False + continue if in_mdx_comment: - if "*/}" in line: - in_mdx_comment = False + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}", True) continue if raw_html_tag is not None: - if re.search(rf"", line, re.IGNORECASE): + if re.search( + r"", line, re.IGNORECASE + ): raw_html_tag = None continue - if in_mdx_esm: - mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) - if not line.strip() and not mdx_esm_template: - in_mdx_esm = False + if raw_html_terminator is not None: + if raw_html_terminator in line: + raw_html_terminator = None + continue + if raw_html_until_blank: + if _is_ascii_blank_line(line): + raw_html_until_blank = False + continue + if in_mdx_esm or in_mdx_expression: continue + list_item = re.match( + r"^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(.*)$", + line.rstrip("\r\n"), + ) + if list_item is not None: + nested = list_item.group(1) + nested_fence = _fence_marker(nested) + nested_raw = re.match( + r"^<(?:pre|script|style|textarea)(?:[ \t]|>|$)", + nested, + re.IGNORECASE, + ) or re.match( + r"^|$)", nested + ) + if ( + nested_fence is not None + and ( + nested_fence[0] != "`" or "`" not in nested_fence[2] + ) + ) or nested_raw is not None: + uncertain = True + break marker = _fence_marker(line) if fence is not None: if ( marker is not None and marker[0] == fence[0] and marker[1] >= fence[1] - and not marker[2].strip() + and not marker[2].strip(" \t") ): fence = None continue - indented_code = _leading_indent_columns(line) >= 4 - if indented_code: - continue - stripped = line.lstrip() - if not stripped: - continue - if marker is not None: + if inline_code_length: + interrupt = line.lstrip(" \t") + interrupt_marker = _fence_marker(line) + if _is_ascii_blank_line(line): + inline_code_length = 0 + continue + if ( + re.match(r"^#(?:[ \t]+|$)", interrupt) + or ( + interrupt_marker is not None + and ( + interrupt_marker[0] != "`" + or "`" not in interrupt_marker[2] + ) + ) + or re.match(r"^") + if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue - if component_document and "{/*" in line: - in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + pre_stripped = line.lstrip(" \t") + if component_document and re.match( + r"^(?:import|export)(?:\s|\{|$)", pre_stripped + ): + uncertain = True + in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + continue + jsx_match = re.match( + r"^|$)", pre_stripped + ) + if component_document and jsx_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True continue raw_match = re.match( - r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + r"^<(pre|script|style|textarea)(?:[ \t]|>|$)", + pre_stripped, + re.IGNORECASE, ) if raw_match is not None: - tag = raw_match.group(1) - if re.search(rf"", stripped, re.IGNORECASE) is None: - raw_html_tag = tag + if re.search( + r"", + pre_stripped, + re.IGNORECASE, + ) is None: + raw_html_tag = raw_match.group(1) + continue + if pre_stripped.startswith("" not in pre_stripped[2:]: + raw_html_terminator = "?>" + continue + if pre_stripped.startswith("" not in pre_stripped[9:]: + raw_html_terminator = "]]>" + continue + if re.match(r"^" not in pre_stripped[2:]: + raw_html_terminator = ">" + continue + block_match = re.match( + r"^|$)", pre_stripped + ) + if ( + block_match is not None + and block_match.group(1).casefold() in _RAW_HTML_BLANK_TAGS + and not ( + component_document and block_match.group(1)[0].isupper() + ) + ): + raw_html_until_blank = True + continue + if component_document and block_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True + continue + if not component_document and block_match is not None: + uncertain = True + raw_html_until_blank = True + continue + leading = len(line) - len(line.lstrip(" \t")) + html_comment = _find_unescaped(line, "") + html_comment_block = in_html_comment + comment_end = _comment_close_end(line, html_comment, "") + remainder = line[comment_end:] if comment_end >= 0 else "" + if ( + not in_html_comment + and component_document + and ( + _find_unescaped(remainder, "{") >= 0 + or re.search(r"= 0 else "" + if ( + not in_mdx_comment + and ( + _find_unescaped(remainder, "{") >= 0 + or _find_unescaped(remainder, "") + if end < 0: + in_html_comment = True + html_comment_block = False + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + if kind == "mdx-comment": + end = _comment_close_end(scan_line, position, "{/*", "*/}") + if end < 0: + in_mdx_comment = True + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + uncertain = True + in_mdx_expression = True + break + if literal_h1 is False and ( + uncertain or in_mdx_esm or in_mdx_expression or inline_code_length + ): literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") diff --git a/adapters/copilot/scripts/evidence_receipt.py b/adapters/copilot/scripts/evidence_receipt.py index b79532a..555bd09 100644 --- a/adapters/copilot/scripts/evidence_receipt.py +++ b/adapters/copilot/scripts/evidence_receipt.py @@ -3,10 +3,14 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import subprocess -import sys from pathlib import Path from _docs_checker.evidence import ( @@ -19,6 +23,9 @@ from _docs_checker.init_adoption import SKILL_VERSION from check import check +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + _PARSER = argparse.ArgumentParser() _PARSER.add_argument("root") diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index bcb1ac2..2c0fb67 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -39,6 +39,7 @@ ) _NETWORK_PATH = re.compile(r"(?= 0 and line.find(closing, start + len(opening)) < 0 +def _comment_remains_open(line, opening, closing, already_open=False): + position = 0 + if already_open: + end = line.find(closing) + if end < 0: + return True + position = end + len(closing) + while True: + start = _find_unescaped(line, opening, position) + if start < 0: + return False + if opening == "", start): + position = start + len("") + continue + if ( + start > len(line) - len(line.lstrip(" \t")) + and line.startswith("", start) + ): + position = start + len("") + continue + end = line.find(closing, start + len(opening)) + if end < 0: + return True + position = end + len(closing) -def _toggle_unescaped_backticks(line, open_state): +def _comment_close_end(line, start, opening, closing): + if opening == "", start): + return start + len("") + if start > len(line) - len(line.lstrip(" \t")) and line.startswith("", start): + return start + len("") + end = line.find(closing, start + len(opening)) + return -1 if end < 0 else end + len(closing) + + +def _tag_close_end(line, start): + quote = None escaped = False - for char in line: - if char == "\\": - escaped = not escaped + for index in range(start, len(line)): + char = line[index] + if quote is not None: + if char == "\\" and not escaped: + escaped = True + continue + if char == quote and not escaped: + quote = None + escaped = False continue - if char == "`" and not escaped: - open_state = not open_state - escaped = False - return open_state + if char in {'"', "'"}: + quote = char + elif char == "{": + return -1 + elif char == ">": + return index + 1 + return -1 + + +def _find_unescaped(value, token, start=0): + position = start + while True: + position = value.find(token, position) + if position < 0: + return -1 + slashes = 0 + cursor = position - 1 + while cursor >= 0 and value[cursor] == "\\": + slashes += 1 + cursor -= 1 + if slashes % 2 == 0: + return position + position += len(token) + + +def _find_tick_run(value, length, start=0): + marker = "`" * length + position = start + while True: + position = value.find(marker, position) + if position < 0: + return -1 + before = position > 0 and value[position - 1] == "`" + after_position = position + length + after = after_position < len(value) and value[after_position] == "`" + if not before and not after: + return position + position = after_position + + +def _without_inline_code_spans(line, open_length=0): + """Mask CommonMark backtick spans on one line and return multiline state.""" + masked = list(line) + index = 0 + if open_length: + close = _find_tick_run(line, open_length) + if close < 0: + for position, char in enumerate(masked): + if char not in "\r\n": + masked[position] = "x" + return "".join(masked), open_length + for position in range(close + open_length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + open_length + while index < len(line): + start = _find_unescaped(line, "`", index) + if start < 0: + break + index = start + if line[index] != "`": + index += 1 + continue + while index < len(line) and line[index] == "`": + index += 1 + length = index - start + close = _find_tick_run(line, length, index) + if close < 0: + for position in range(start, len(masked)): + if masked[position] not in "\r\n": + masked[position] = "x" + return "".join(masked), length + for position in range(start, close + length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + length + return "".join(masked), 0 + + +def _is_single_line_mdx_esm(line): + stripped = line.strip() + simple_import = re.fullmatch( + r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + stripped, + ) + simple_export = re.fullmatch( + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + stripped, + ) + return simple_import is not None or simple_export is not None + + +_RAW_HTML_BLANK_TAGS = frozenset( + { + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", + "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", + "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", + "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", + "track", "ul", + } +) def _fence_marker(line): @@ -584,6 +735,10 @@ def _leading_indent_columns(line): return columns +def _is_ascii_blank_line(line): + return not line.rstrip("\r\n").strip(" \t") + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -598,7 +753,8 @@ def observe_entry_orientation(root, entry): try: if path.stat().st_size > 2 * 1024 * 1024: raise ValueError("entry exceeds capacity") - text = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as stream: + text = stream.read() except (OSError, UnicodeError, ValueError): return { "literal_h1": evidence_value("unavailable"), @@ -610,74 +766,293 @@ def observe_entry_orientation(root, entry): if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" fence = None + inline_code_length = 0 in_html_comment = False + html_comment_block = False in_mdx_comment = False raw_html_tag = None + raw_html_terminator = None + raw_html_until_blank = False in_mdx_esm = False - mdx_esm_template = False + in_mdx_expression = False uncertain = False literal_h1 = False - for line in body_lines: + for raw_line in body_lines: + line = raw_line.rstrip("\r\n") if in_html_comment: - if "-->" in line: + if not html_comment_block and ( + _is_ascii_blank_line(line) + or ( + _leading_indent_columns(line) < 4 + and re.match( + r"^#(?:[ \t]+|$)", line.lstrip(" \t") + ) + ) + ): in_html_comment = False - continue + html_comment_block = False + if _is_ascii_blank_line(line): + continue + else: + in_html_comment = _comment_remains_open( + line, "", True + ) + if not in_html_comment: + html_comment_block = False + continue if in_mdx_comment: - if "*/}" in line: - in_mdx_comment = False + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}", True) continue if raw_html_tag is not None: - if re.search(rf"", line, re.IGNORECASE): + if re.search( + r"", line, re.IGNORECASE + ): raw_html_tag = None continue - if in_mdx_esm: - mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) - if not line.strip() and not mdx_esm_template: - in_mdx_esm = False + if raw_html_terminator is not None: + if raw_html_terminator in line: + raw_html_terminator = None + continue + if raw_html_until_blank: + if _is_ascii_blank_line(line): + raw_html_until_blank = False + continue + if in_mdx_esm or in_mdx_expression: continue + list_item = re.match( + r"^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(.*)$", + line.rstrip("\r\n"), + ) + if list_item is not None: + nested = list_item.group(1) + nested_fence = _fence_marker(nested) + nested_raw = re.match( + r"^<(?:pre|script|style|textarea)(?:[ \t]|>|$)", + nested, + re.IGNORECASE, + ) or re.match( + r"^|$)", nested + ) + if ( + nested_fence is not None + and ( + nested_fence[0] != "`" or "`" not in nested_fence[2] + ) + ) or nested_raw is not None: + uncertain = True + break marker = _fence_marker(line) if fence is not None: if ( marker is not None and marker[0] == fence[0] and marker[1] >= fence[1] - and not marker[2].strip() + and not marker[2].strip(" \t") ): fence = None continue - indented_code = _leading_indent_columns(line) >= 4 - if indented_code: - continue - stripped = line.lstrip() - if not stripped: - continue - if marker is not None: + if inline_code_length: + interrupt = line.lstrip(" \t") + interrupt_marker = _fence_marker(line) + if _is_ascii_blank_line(line): + inline_code_length = 0 + continue + if ( + re.match(r"^#(?:[ \t]+|$)", interrupt) + or ( + interrupt_marker is not None + and ( + interrupt_marker[0] != "`" + or "`" not in interrupt_marker[2] + ) + ) + or re.match(r"^") + if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue - if component_document and "{/*" in line: - in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + pre_stripped = line.lstrip(" \t") + if component_document and re.match( + r"^(?:import|export)(?:\s|\{|$)", pre_stripped + ): + uncertain = True + in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + continue + jsx_match = re.match( + r"^|$)", pre_stripped + ) + if component_document and jsx_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True continue raw_match = re.match( - r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + r"^<(pre|script|style|textarea)(?:[ \t]|>|$)", + pre_stripped, + re.IGNORECASE, ) if raw_match is not None: - tag = raw_match.group(1) - if re.search(rf"", stripped, re.IGNORECASE) is None: - raw_html_tag = tag + if re.search( + r"", + pre_stripped, + re.IGNORECASE, + ) is None: + raw_html_tag = raw_match.group(1) + continue + if pre_stripped.startswith("" not in pre_stripped[2:]: + raw_html_terminator = "?>" + continue + if pre_stripped.startswith("" not in pre_stripped[9:]: + raw_html_terminator = "]]>" + continue + if re.match(r"^" not in pre_stripped[2:]: + raw_html_terminator = ">" + continue + block_match = re.match( + r"^|$)", pre_stripped + ) + if ( + block_match is not None + and block_match.group(1).casefold() in _RAW_HTML_BLANK_TAGS + and not ( + component_document and block_match.group(1)[0].isupper() + ) + ): + raw_html_until_blank = True + continue + if component_document and block_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True + continue + if not component_document and block_match is not None: + uncertain = True + raw_html_until_blank = True + continue + leading = len(line) - len(line.lstrip(" \t")) + html_comment = _find_unescaped(line, "") + html_comment_block = in_html_comment + comment_end = _comment_close_end(line, html_comment, "") + remainder = line[comment_end:] if comment_end >= 0 else "" + if ( + not in_html_comment + and component_document + and ( + _find_unescaped(remainder, "{") >= 0 + or re.search(r"= 0 else "" + if ( + not in_mdx_comment + and ( + _find_unescaped(remainder, "{") >= 0 + or _find_unescaped(remainder, "") + if end < 0: + in_html_comment = True + html_comment_block = False + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + if kind == "mdx-comment": + end = _comment_close_end(scan_line, position, "{/*", "*/}") + if end < 0: + in_mdx_comment = True + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + uncertain = True + in_mdx_expression = True + break + if literal_h1 is False and ( + uncertain or in_mdx_esm or in_mdx_expression or inline_code_length + ): literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") diff --git a/adapters/cursor/scripts/evidence_receipt.py b/adapters/cursor/scripts/evidence_receipt.py index b79532a..555bd09 100644 --- a/adapters/cursor/scripts/evidence_receipt.py +++ b/adapters/cursor/scripts/evidence_receipt.py @@ -3,10 +3,14 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import subprocess -import sys from pathlib import Path from _docs_checker.evidence import ( @@ -19,6 +23,9 @@ from _docs_checker.init_adoption import SKILL_VERSION from check import check +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + _PARSER = argparse.ArgumentParser() _PARSER.add_argument("root") diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index bcb1ac2..2c0fb67 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -39,6 +39,7 @@ ) _NETWORK_PATH = re.compile(r"(?= 0 and line.find(closing, start + len(opening)) < 0 +def _comment_remains_open(line, opening, closing, already_open=False): + position = 0 + if already_open: + end = line.find(closing) + if end < 0: + return True + position = end + len(closing) + while True: + start = _find_unescaped(line, opening, position) + if start < 0: + return False + if opening == "", start): + position = start + len("") + continue + if ( + start > len(line) - len(line.lstrip(" \t")) + and line.startswith("", start) + ): + position = start + len("") + continue + end = line.find(closing, start + len(opening)) + if end < 0: + return True + position = end + len(closing) -def _toggle_unescaped_backticks(line, open_state): +def _comment_close_end(line, start, opening, closing): + if opening == "", start): + return start + len("") + if start > len(line) - len(line.lstrip(" \t")) and line.startswith("", start): + return start + len("") + end = line.find(closing, start + len(opening)) + return -1 if end < 0 else end + len(closing) + + +def _tag_close_end(line, start): + quote = None escaped = False - for char in line: - if char == "\\": - escaped = not escaped + for index in range(start, len(line)): + char = line[index] + if quote is not None: + if char == "\\" and not escaped: + escaped = True + continue + if char == quote and not escaped: + quote = None + escaped = False continue - if char == "`" and not escaped: - open_state = not open_state - escaped = False - return open_state + if char in {'"', "'"}: + quote = char + elif char == "{": + return -1 + elif char == ">": + return index + 1 + return -1 + + +def _find_unescaped(value, token, start=0): + position = start + while True: + position = value.find(token, position) + if position < 0: + return -1 + slashes = 0 + cursor = position - 1 + while cursor >= 0 and value[cursor] == "\\": + slashes += 1 + cursor -= 1 + if slashes % 2 == 0: + return position + position += len(token) + + +def _find_tick_run(value, length, start=0): + marker = "`" * length + position = start + while True: + position = value.find(marker, position) + if position < 0: + return -1 + before = position > 0 and value[position - 1] == "`" + after_position = position + length + after = after_position < len(value) and value[after_position] == "`" + if not before and not after: + return position + position = after_position + + +def _without_inline_code_spans(line, open_length=0): + """Mask CommonMark backtick spans on one line and return multiline state.""" + masked = list(line) + index = 0 + if open_length: + close = _find_tick_run(line, open_length) + if close < 0: + for position, char in enumerate(masked): + if char not in "\r\n": + masked[position] = "x" + return "".join(masked), open_length + for position in range(close + open_length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + open_length + while index < len(line): + start = _find_unescaped(line, "`", index) + if start < 0: + break + index = start + if line[index] != "`": + index += 1 + continue + while index < len(line) and line[index] == "`": + index += 1 + length = index - start + close = _find_tick_run(line, length, index) + if close < 0: + for position in range(start, len(masked)): + if masked[position] not in "\r\n": + masked[position] = "x" + return "".join(masked), length + for position in range(start, close + length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + length + return "".join(masked), 0 + + +def _is_single_line_mdx_esm(line): + stripped = line.strip() + simple_import = re.fullmatch( + r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + stripped, + ) + simple_export = re.fullmatch( + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + stripped, + ) + return simple_import is not None or simple_export is not None + + +_RAW_HTML_BLANK_TAGS = frozenset( + { + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", + "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", + "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", + "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", + "track", "ul", + } +) def _fence_marker(line): @@ -584,6 +735,10 @@ def _leading_indent_columns(line): return columns +def _is_ascii_blank_line(line): + return not line.rstrip("\r\n").strip(" \t") + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -598,7 +753,8 @@ def observe_entry_orientation(root, entry): try: if path.stat().st_size > 2 * 1024 * 1024: raise ValueError("entry exceeds capacity") - text = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as stream: + text = stream.read() except (OSError, UnicodeError, ValueError): return { "literal_h1": evidence_value("unavailable"), @@ -610,74 +766,293 @@ def observe_entry_orientation(root, entry): if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" fence = None + inline_code_length = 0 in_html_comment = False + html_comment_block = False in_mdx_comment = False raw_html_tag = None + raw_html_terminator = None + raw_html_until_blank = False in_mdx_esm = False - mdx_esm_template = False + in_mdx_expression = False uncertain = False literal_h1 = False - for line in body_lines: + for raw_line in body_lines: + line = raw_line.rstrip("\r\n") if in_html_comment: - if "-->" in line: + if not html_comment_block and ( + _is_ascii_blank_line(line) + or ( + _leading_indent_columns(line) < 4 + and re.match( + r"^#(?:[ \t]+|$)", line.lstrip(" \t") + ) + ) + ): in_html_comment = False - continue + html_comment_block = False + if _is_ascii_blank_line(line): + continue + else: + in_html_comment = _comment_remains_open( + line, "", True + ) + if not in_html_comment: + html_comment_block = False + continue if in_mdx_comment: - if "*/}" in line: - in_mdx_comment = False + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}", True) continue if raw_html_tag is not None: - if re.search(rf"", line, re.IGNORECASE): + if re.search( + r"", line, re.IGNORECASE + ): raw_html_tag = None continue - if in_mdx_esm: - mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) - if not line.strip() and not mdx_esm_template: - in_mdx_esm = False + if raw_html_terminator is not None: + if raw_html_terminator in line: + raw_html_terminator = None + continue + if raw_html_until_blank: + if _is_ascii_blank_line(line): + raw_html_until_blank = False + continue + if in_mdx_esm or in_mdx_expression: continue + list_item = re.match( + r"^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(.*)$", + line.rstrip("\r\n"), + ) + if list_item is not None: + nested = list_item.group(1) + nested_fence = _fence_marker(nested) + nested_raw = re.match( + r"^<(?:pre|script|style|textarea)(?:[ \t]|>|$)", + nested, + re.IGNORECASE, + ) or re.match( + r"^|$)", nested + ) + if ( + nested_fence is not None + and ( + nested_fence[0] != "`" or "`" not in nested_fence[2] + ) + ) or nested_raw is not None: + uncertain = True + break marker = _fence_marker(line) if fence is not None: if ( marker is not None and marker[0] == fence[0] and marker[1] >= fence[1] - and not marker[2].strip() + and not marker[2].strip(" \t") ): fence = None continue - indented_code = _leading_indent_columns(line) >= 4 - if indented_code: - continue - stripped = line.lstrip() - if not stripped: - continue - if marker is not None: + if inline_code_length: + interrupt = line.lstrip(" \t") + interrupt_marker = _fence_marker(line) + if _is_ascii_blank_line(line): + inline_code_length = 0 + continue + if ( + re.match(r"^#(?:[ \t]+|$)", interrupt) + or ( + interrupt_marker is not None + and ( + interrupt_marker[0] != "`" + or "`" not in interrupt_marker[2] + ) + ) + or re.match(r"^") + if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue - if component_document and "{/*" in line: - in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + pre_stripped = line.lstrip(" \t") + if component_document and re.match( + r"^(?:import|export)(?:\s|\{|$)", pre_stripped + ): + uncertain = True + in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + continue + jsx_match = re.match( + r"^|$)", pre_stripped + ) + if component_document and jsx_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True continue raw_match = re.match( - r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + r"^<(pre|script|style|textarea)(?:[ \t]|>|$)", + pre_stripped, + re.IGNORECASE, ) if raw_match is not None: - tag = raw_match.group(1) - if re.search(rf"", stripped, re.IGNORECASE) is None: - raw_html_tag = tag + if re.search( + r"", + pre_stripped, + re.IGNORECASE, + ) is None: + raw_html_tag = raw_match.group(1) + continue + if pre_stripped.startswith("" not in pre_stripped[2:]: + raw_html_terminator = "?>" + continue + if pre_stripped.startswith("" not in pre_stripped[9:]: + raw_html_terminator = "]]>" + continue + if re.match(r"^" not in pre_stripped[2:]: + raw_html_terminator = ">" + continue + block_match = re.match( + r"^|$)", pre_stripped + ) + if ( + block_match is not None + and block_match.group(1).casefold() in _RAW_HTML_BLANK_TAGS + and not ( + component_document and block_match.group(1)[0].isupper() + ) + ): + raw_html_until_blank = True + continue + if component_document and block_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True + continue + if not component_document and block_match is not None: + uncertain = True + raw_html_until_blank = True + continue + leading = len(line) - len(line.lstrip(" \t")) + html_comment = _find_unescaped(line, "") + html_comment_block = in_html_comment + comment_end = _comment_close_end(line, html_comment, "") + remainder = line[comment_end:] if comment_end >= 0 else "" + if ( + not in_html_comment + and component_document + and ( + _find_unescaped(remainder, "{") >= 0 + or re.search(r"= 0 else "" + if ( + not in_mdx_comment + and ( + _find_unescaped(remainder, "{") >= 0 + or _find_unescaped(remainder, "") + if end < 0: + in_html_comment = True + html_comment_block = False + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + if kind == "mdx-comment": + end = _comment_close_end(scan_line, position, "{/*", "*/}") + if end < 0: + in_mdx_comment = True + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + uncertain = True + in_mdx_expression = True + break + if literal_h1 is False and ( + uncertain or in_mdx_esm or in_mdx_expression or inline_code_length + ): literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") diff --git a/adapters/grok/scripts/evidence_receipt.py b/adapters/grok/scripts/evidence_receipt.py index b79532a..555bd09 100644 --- a/adapters/grok/scripts/evidence_receipt.py +++ b/adapters/grok/scripts/evidence_receipt.py @@ -3,10 +3,14 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import subprocess -import sys from pathlib import Path from _docs_checker.evidence import ( @@ -19,6 +23,9 @@ from _docs_checker.init_adoption import SKILL_VERSION from check import check +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + _PARSER = argparse.ArgumentParser() _PARSER.add_argument("root") diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index bcb1ac2..2c0fb67 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -39,6 +39,7 @@ ) _NETWORK_PATH = re.compile(r"(?= 0 and line.find(closing, start + len(opening)) < 0 +def _comment_remains_open(line, opening, closing, already_open=False): + position = 0 + if already_open: + end = line.find(closing) + if end < 0: + return True + position = end + len(closing) + while True: + start = _find_unescaped(line, opening, position) + if start < 0: + return False + if opening == "", start): + position = start + len("") + continue + if ( + start > len(line) - len(line.lstrip(" \t")) + and line.startswith("", start) + ): + position = start + len("") + continue + end = line.find(closing, start + len(opening)) + if end < 0: + return True + position = end + len(closing) -def _toggle_unescaped_backticks(line, open_state): +def _comment_close_end(line, start, opening, closing): + if opening == "", start): + return start + len("") + if start > len(line) - len(line.lstrip(" \t")) and line.startswith("", start): + return start + len("") + end = line.find(closing, start + len(opening)) + return -1 if end < 0 else end + len(closing) + + +def _tag_close_end(line, start): + quote = None escaped = False - for char in line: - if char == "\\": - escaped = not escaped + for index in range(start, len(line)): + char = line[index] + if quote is not None: + if char == "\\" and not escaped: + escaped = True + continue + if char == quote and not escaped: + quote = None + escaped = False continue - if char == "`" and not escaped: - open_state = not open_state - escaped = False - return open_state + if char in {'"', "'"}: + quote = char + elif char == "{": + return -1 + elif char == ">": + return index + 1 + return -1 + + +def _find_unescaped(value, token, start=0): + position = start + while True: + position = value.find(token, position) + if position < 0: + return -1 + slashes = 0 + cursor = position - 1 + while cursor >= 0 and value[cursor] == "\\": + slashes += 1 + cursor -= 1 + if slashes % 2 == 0: + return position + position += len(token) + + +def _find_tick_run(value, length, start=0): + marker = "`" * length + position = start + while True: + position = value.find(marker, position) + if position < 0: + return -1 + before = position > 0 and value[position - 1] == "`" + after_position = position + length + after = after_position < len(value) and value[after_position] == "`" + if not before and not after: + return position + position = after_position + + +def _without_inline_code_spans(line, open_length=0): + """Mask CommonMark backtick spans on one line and return multiline state.""" + masked = list(line) + index = 0 + if open_length: + close = _find_tick_run(line, open_length) + if close < 0: + for position, char in enumerate(masked): + if char not in "\r\n": + masked[position] = "x" + return "".join(masked), open_length + for position in range(close + open_length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + open_length + while index < len(line): + start = _find_unescaped(line, "`", index) + if start < 0: + break + index = start + if line[index] != "`": + index += 1 + continue + while index < len(line) and line[index] == "`": + index += 1 + length = index - start + close = _find_tick_run(line, length, index) + if close < 0: + for position in range(start, len(masked)): + if masked[position] not in "\r\n": + masked[position] = "x" + return "".join(masked), length + for position in range(start, close + length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + length + return "".join(masked), 0 + + +def _is_single_line_mdx_esm(line): + stripped = line.strip() + simple_import = re.fullmatch( + r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + stripped, + ) + simple_export = re.fullmatch( + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + stripped, + ) + return simple_import is not None or simple_export is not None + + +_RAW_HTML_BLANK_TAGS = frozenset( + { + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", + "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", + "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", + "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", + "track", "ul", + } +) def _fence_marker(line): @@ -584,6 +735,10 @@ def _leading_indent_columns(line): return columns +def _is_ascii_blank_line(line): + return not line.rstrip("\r\n").strip(" \t") + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -598,7 +753,8 @@ def observe_entry_orientation(root, entry): try: if path.stat().st_size > 2 * 1024 * 1024: raise ValueError("entry exceeds capacity") - text = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as stream: + text = stream.read() except (OSError, UnicodeError, ValueError): return { "literal_h1": evidence_value("unavailable"), @@ -610,74 +766,293 @@ def observe_entry_orientation(root, entry): if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" fence = None + inline_code_length = 0 in_html_comment = False + html_comment_block = False in_mdx_comment = False raw_html_tag = None + raw_html_terminator = None + raw_html_until_blank = False in_mdx_esm = False - mdx_esm_template = False + in_mdx_expression = False uncertain = False literal_h1 = False - for line in body_lines: + for raw_line in body_lines: + line = raw_line.rstrip("\r\n") if in_html_comment: - if "-->" in line: + if not html_comment_block and ( + _is_ascii_blank_line(line) + or ( + _leading_indent_columns(line) < 4 + and re.match( + r"^#(?:[ \t]+|$)", line.lstrip(" \t") + ) + ) + ): in_html_comment = False - continue + html_comment_block = False + if _is_ascii_blank_line(line): + continue + else: + in_html_comment = _comment_remains_open( + line, "", True + ) + if not in_html_comment: + html_comment_block = False + continue if in_mdx_comment: - if "*/}" in line: - in_mdx_comment = False + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}", True) continue if raw_html_tag is not None: - if re.search(rf"", line, re.IGNORECASE): + if re.search( + r"", line, re.IGNORECASE + ): raw_html_tag = None continue - if in_mdx_esm: - mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) - if not line.strip() and not mdx_esm_template: - in_mdx_esm = False + if raw_html_terminator is not None: + if raw_html_terminator in line: + raw_html_terminator = None + continue + if raw_html_until_blank: + if _is_ascii_blank_line(line): + raw_html_until_blank = False + continue + if in_mdx_esm or in_mdx_expression: continue + list_item = re.match( + r"^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(.*)$", + line.rstrip("\r\n"), + ) + if list_item is not None: + nested = list_item.group(1) + nested_fence = _fence_marker(nested) + nested_raw = re.match( + r"^<(?:pre|script|style|textarea)(?:[ \t]|>|$)", + nested, + re.IGNORECASE, + ) or re.match( + r"^|$)", nested + ) + if ( + nested_fence is not None + and ( + nested_fence[0] != "`" or "`" not in nested_fence[2] + ) + ) or nested_raw is not None: + uncertain = True + break marker = _fence_marker(line) if fence is not None: if ( marker is not None and marker[0] == fence[0] and marker[1] >= fence[1] - and not marker[2].strip() + and not marker[2].strip(" \t") ): fence = None continue - indented_code = _leading_indent_columns(line) >= 4 - if indented_code: - continue - stripped = line.lstrip() - if not stripped: - continue - if marker is not None: + if inline_code_length: + interrupt = line.lstrip(" \t") + interrupt_marker = _fence_marker(line) + if _is_ascii_blank_line(line): + inline_code_length = 0 + continue + if ( + re.match(r"^#(?:[ \t]+|$)", interrupt) + or ( + interrupt_marker is not None + and ( + interrupt_marker[0] != "`" + or "`" not in interrupt_marker[2] + ) + ) + or re.match(r"^") + if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue - if component_document and "{/*" in line: - in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + pre_stripped = line.lstrip(" \t") + if component_document and re.match( + r"^(?:import|export)(?:\s|\{|$)", pre_stripped + ): + uncertain = True + in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + continue + jsx_match = re.match( + r"^|$)", pre_stripped + ) + if component_document and jsx_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True continue raw_match = re.match( - r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + r"^<(pre|script|style|textarea)(?:[ \t]|>|$)", + pre_stripped, + re.IGNORECASE, ) if raw_match is not None: - tag = raw_match.group(1) - if re.search(rf"", stripped, re.IGNORECASE) is None: - raw_html_tag = tag + if re.search( + r"", + pre_stripped, + re.IGNORECASE, + ) is None: + raw_html_tag = raw_match.group(1) + continue + if pre_stripped.startswith("" not in pre_stripped[2:]: + raw_html_terminator = "?>" + continue + if pre_stripped.startswith("" not in pre_stripped[9:]: + raw_html_terminator = "]]>" + continue + if re.match(r"^" not in pre_stripped[2:]: + raw_html_terminator = ">" + continue + block_match = re.match( + r"^|$)", pre_stripped + ) + if ( + block_match is not None + and block_match.group(1).casefold() in _RAW_HTML_BLANK_TAGS + and not ( + component_document and block_match.group(1)[0].isupper() + ) + ): + raw_html_until_blank = True + continue + if component_document and block_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True + continue + if not component_document and block_match is not None: + uncertain = True + raw_html_until_blank = True + continue + leading = len(line) - len(line.lstrip(" \t")) + html_comment = _find_unescaped(line, "") + html_comment_block = in_html_comment + comment_end = _comment_close_end(line, html_comment, "") + remainder = line[comment_end:] if comment_end >= 0 else "" + if ( + not in_html_comment + and component_document + and ( + _find_unescaped(remainder, "{") >= 0 + or re.search(r"= 0 else "" + if ( + not in_mdx_comment + and ( + _find_unescaped(remainder, "{") >= 0 + or _find_unescaped(remainder, "") + if end < 0: + in_html_comment = True + html_comment_block = False + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + if kind == "mdx-comment": + end = _comment_close_end(scan_line, position, "{/*", "*/}") + if end < 0: + in_mdx_comment = True + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + uncertain = True + in_mdx_expression = True + break + if literal_h1 is False and ( + uncertain or in_mdx_esm or in_mdx_expression or inline_code_length + ): literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") diff --git a/adapters/plugin/skills/docs/scripts/evidence_receipt.py b/adapters/plugin/skills/docs/scripts/evidence_receipt.py index b79532a..555bd09 100644 --- a/adapters/plugin/skills/docs/scripts/evidence_receipt.py +++ b/adapters/plugin/skills/docs/scripts/evidence_receipt.py @@ -3,10 +3,14 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import subprocess -import sys from pathlib import Path from _docs_checker.evidence import ( @@ -19,6 +23,9 @@ from _docs_checker.init_adoption import SKILL_VERSION from check import check +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + _PARSER = argparse.ArgumentParser() _PARSER.add_argument("root") diff --git a/evals/docs-corpus-baseline-v1.json b/evals/docs-corpus-baseline-v1.json index 068a6e8..3bd13a3 100644 --- a/evals/docs-corpus-baseline-v1.json +++ b/evals/docs-corpus-baseline-v1.json @@ -391,8 +391,8 @@ "value": true }, "literal_h1": { - "status": "completed", - "value": false + "status": "unavailable", + "value": null }, "provider_rendered_title": { "status": "unavailable", @@ -459,6 +459,7 @@ "evidence.semantic.evaluator.model", "evidence.semantic.evaluator.provider", "evidence.semantic.evaluator.version", + "orientation.literal_h1", "orientation.provider_rendered_title", "run.duration_seconds" ], @@ -619,8 +620,8 @@ "value": true }, "literal_h1": { - "status": "completed", - "value": false + "status": "unavailable", + "value": null }, "provider_rendered_title": { "status": "unavailable", @@ -688,6 +689,7 @@ "health.rubric_version", "health.score_gates.map_has_h1", "health.score_gates.useful_entry", + "orientation.literal_h1", "orientation.provider_rendered_title", "run.duration_seconds" ], @@ -1077,8 +1079,8 @@ "value": true }, "literal_h1": { - "status": "completed", - "value": false + "status": "unavailable", + "value": null }, "provider_rendered_title": { "status": "unavailable", @@ -1146,6 +1148,7 @@ "health.rubric_version", "health.score_gates.map_has_h1", "health.score_gates.useful_entry", + "orientation.literal_h1", "orientation.provider_rendered_title", "run.duration_seconds" ], diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index bcb1ac2..2c0fb67 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -39,6 +39,7 @@ ) _NETWORK_PATH = re.compile(r"(?= 0 and line.find(closing, start + len(opening)) < 0 +def _comment_remains_open(line, opening, closing, already_open=False): + position = 0 + if already_open: + end = line.find(closing) + if end < 0: + return True + position = end + len(closing) + while True: + start = _find_unescaped(line, opening, position) + if start < 0: + return False + if opening == "", start): + position = start + len("") + continue + if ( + start > len(line) - len(line.lstrip(" \t")) + and line.startswith("", start) + ): + position = start + len("") + continue + end = line.find(closing, start + len(opening)) + if end < 0: + return True + position = end + len(closing) -def _toggle_unescaped_backticks(line, open_state): +def _comment_close_end(line, start, opening, closing): + if opening == "", start): + return start + len("") + if start > len(line) - len(line.lstrip(" \t")) and line.startswith("", start): + return start + len("") + end = line.find(closing, start + len(opening)) + return -1 if end < 0 else end + len(closing) + + +def _tag_close_end(line, start): + quote = None escaped = False - for char in line: - if char == "\\": - escaped = not escaped + for index in range(start, len(line)): + char = line[index] + if quote is not None: + if char == "\\" and not escaped: + escaped = True + continue + if char == quote and not escaped: + quote = None + escaped = False continue - if char == "`" and not escaped: - open_state = not open_state - escaped = False - return open_state + if char in {'"', "'"}: + quote = char + elif char == "{": + return -1 + elif char == ">": + return index + 1 + return -1 + + +def _find_unescaped(value, token, start=0): + position = start + while True: + position = value.find(token, position) + if position < 0: + return -1 + slashes = 0 + cursor = position - 1 + while cursor >= 0 and value[cursor] == "\\": + slashes += 1 + cursor -= 1 + if slashes % 2 == 0: + return position + position += len(token) + + +def _find_tick_run(value, length, start=0): + marker = "`" * length + position = start + while True: + position = value.find(marker, position) + if position < 0: + return -1 + before = position > 0 and value[position - 1] == "`" + after_position = position + length + after = after_position < len(value) and value[after_position] == "`" + if not before and not after: + return position + position = after_position + + +def _without_inline_code_spans(line, open_length=0): + """Mask CommonMark backtick spans on one line and return multiline state.""" + masked = list(line) + index = 0 + if open_length: + close = _find_tick_run(line, open_length) + if close < 0: + for position, char in enumerate(masked): + if char not in "\r\n": + masked[position] = "x" + return "".join(masked), open_length + for position in range(close + open_length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + open_length + while index < len(line): + start = _find_unescaped(line, "`", index) + if start < 0: + break + index = start + if line[index] != "`": + index += 1 + continue + while index < len(line) and line[index] == "`": + index += 1 + length = index - start + close = _find_tick_run(line, length, index) + if close < 0: + for position in range(start, len(masked)): + if masked[position] not in "\r\n": + masked[position] = "x" + return "".join(masked), length + for position in range(start, close + length): + if masked[position] not in "\r\n": + masked[position] = "x" + index = close + length + return "".join(masked), 0 + + +def _is_single_line_mdx_esm(line): + stripped = line.strip() + simple_import = re.fullmatch( + r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + stripped, + ) + simple_export = re.fullmatch( + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + stripped, + ) + return simple_import is not None or simple_export is not None + + +_RAW_HTML_BLANK_TAGS = frozenset( + { + "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", + "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", + "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", + "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", + "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", + "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", + "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", + "track", "ul", + } +) def _fence_marker(line): @@ -584,6 +735,10 @@ def _leading_indent_columns(line): return columns +def _is_ascii_blank_line(line): + return not line.rstrip("\r\n").strip(" \t") + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -598,7 +753,8 @@ def observe_entry_orientation(root, entry): try: if path.stat().st_size > 2 * 1024 * 1024: raise ValueError("entry exceeds capacity") - text = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as stream: + text = stream.read() except (OSError, UnicodeError, ValueError): return { "literal_h1": evidence_value("unavailable"), @@ -610,74 +766,293 @@ def observe_entry_orientation(root, entry): if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" fence = None + inline_code_length = 0 in_html_comment = False + html_comment_block = False in_mdx_comment = False raw_html_tag = None + raw_html_terminator = None + raw_html_until_blank = False in_mdx_esm = False - mdx_esm_template = False + in_mdx_expression = False uncertain = False literal_h1 = False - for line in body_lines: + for raw_line in body_lines: + line = raw_line.rstrip("\r\n") if in_html_comment: - if "-->" in line: + if not html_comment_block and ( + _is_ascii_blank_line(line) + or ( + _leading_indent_columns(line) < 4 + and re.match( + r"^#(?:[ \t]+|$)", line.lstrip(" \t") + ) + ) + ): in_html_comment = False - continue + html_comment_block = False + if _is_ascii_blank_line(line): + continue + else: + in_html_comment = _comment_remains_open( + line, "", True + ) + if not in_html_comment: + html_comment_block = False + continue if in_mdx_comment: - if "*/}" in line: - in_mdx_comment = False + in_mdx_comment = _comment_remains_open(line, "{/*", "*/}", True) continue if raw_html_tag is not None: - if re.search(rf"", line, re.IGNORECASE): + if re.search( + r"", line, re.IGNORECASE + ): raw_html_tag = None continue - if in_mdx_esm: - mdx_esm_template = _toggle_unescaped_backticks(line, mdx_esm_template) - if not line.strip() and not mdx_esm_template: - in_mdx_esm = False + if raw_html_terminator is not None: + if raw_html_terminator in line: + raw_html_terminator = None + continue + if raw_html_until_blank: + if _is_ascii_blank_line(line): + raw_html_until_blank = False + continue + if in_mdx_esm or in_mdx_expression: continue + list_item = re.match( + r"^ {0,3}(?:[-+*]|[0-9]{1,9}[.)])[ \t]+(.*)$", + line.rstrip("\r\n"), + ) + if list_item is not None: + nested = list_item.group(1) + nested_fence = _fence_marker(nested) + nested_raw = re.match( + r"^<(?:pre|script|style|textarea)(?:[ \t]|>|$)", + nested, + re.IGNORECASE, + ) or re.match( + r"^|$)", nested + ) + if ( + nested_fence is not None + and ( + nested_fence[0] != "`" or "`" not in nested_fence[2] + ) + ) or nested_raw is not None: + uncertain = True + break marker = _fence_marker(line) if fence is not None: if ( marker is not None and marker[0] == fence[0] and marker[1] >= fence[1] - and not marker[2].strip() + and not marker[2].strip(" \t") ): fence = None continue - indented_code = _leading_indent_columns(line) >= 4 - if indented_code: - continue - stripped = line.lstrip() - if not stripped: - continue - if marker is not None: + if inline_code_length: + interrupt = line.lstrip(" \t") + interrupt_marker = _fence_marker(line) + if _is_ascii_blank_line(line): + inline_code_length = 0 + continue + if ( + re.match(r"^#(?:[ \t]+|$)", interrupt) + or ( + interrupt_marker is not None + and ( + interrupt_marker[0] != "`" + or "`" not in interrupt_marker[2] + ) + ) + or re.match(r"^") + if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue - if component_document and "{/*" in line: - in_mdx_comment = _comment_remains_open(line, "{/*", "*/}") + pre_stripped = line.lstrip(" \t") + if component_document and re.match( + r"^(?:import|export)(?:\s|\{|$)", pre_stripped + ): + uncertain = True + in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + continue + jsx_match = re.match( + r"^|$)", pre_stripped + ) + if component_document and jsx_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True continue raw_match = re.match( - r"^<(pre|script|style|textarea)(?:\s|>|$)", stripped, re.IGNORECASE + r"^<(pre|script|style|textarea)(?:[ \t]|>|$)", + pre_stripped, + re.IGNORECASE, ) if raw_match is not None: - tag = raw_match.group(1) - if re.search(rf"", stripped, re.IGNORECASE) is None: - raw_html_tag = tag + if re.search( + r"", + pre_stripped, + re.IGNORECASE, + ) is None: + raw_html_tag = raw_match.group(1) + continue + if pre_stripped.startswith("" not in pre_stripped[2:]: + raw_html_terminator = "?>" + continue + if pre_stripped.startswith("" not in pre_stripped[9:]: + raw_html_terminator = "]]>" + continue + if re.match(r"^" not in pre_stripped[2:]: + raw_html_terminator = ">" + continue + block_match = re.match( + r"^|$)", pre_stripped + ) + if ( + block_match is not None + and block_match.group(1).casefold() in _RAW_HTML_BLANK_TAGS + and not ( + component_document and block_match.group(1)[0].isupper() + ) + ): + raw_html_until_blank = True + continue + if component_document and block_match is not None: + jsx_end = _tag_close_end(pre_stripped, 0) + if jsx_end < 0 or pre_stripped[jsx_end:].strip(" \t\r\n"): + uncertain = True + in_mdx_expression = True + continue + if not component_document and block_match is not None: + uncertain = True + raw_html_until_blank = True + continue + leading = len(line) - len(line.lstrip(" \t")) + html_comment = _find_unescaped(line, "") + html_comment_block = in_html_comment + comment_end = _comment_close_end(line, html_comment, "") + remainder = line[comment_end:] if comment_end >= 0 else "" + if ( + not in_html_comment + and component_document + and ( + _find_unescaped(remainder, "{") >= 0 + or re.search(r"= 0 else "" + if ( + not in_mdx_comment + and ( + _find_unescaped(remainder, "{") >= 0 + or _find_unescaped(remainder, "") + if end < 0: + in_html_comment = True + html_comment_block = False + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + if kind == "mdx-comment": + end = _comment_close_end(scan_line, position, "{/*", "*/}") + if end < 0: + in_mdx_comment = True + break + scan_line = ( + scan_line[:position] + "x" * (end - position) + scan_line[end:] + ) + continue + uncertain = True + in_mdx_expression = True + break + if literal_h1 is False and ( + uncertain or in_mdx_esm or in_mdx_expression or inline_code_length + ): literal_h1 = None metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) title = metadata.get("values", {}).get("title") diff --git a/skills/docs/scripts/evidence_receipt.py b/skills/docs/scripts/evidence_receipt.py index b79532a..555bd09 100644 --- a/skills/docs/scripts/evidence_receipt.py +++ b/skills/docs/scripts/evidence_receipt.py @@ -3,10 +3,14 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import subprocess -import sys from pathlib import Path from _docs_checker.evidence import ( @@ -19,6 +23,9 @@ from _docs_checker.init_adoption import SKILL_VERSION from check import check +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + _PARSER = argparse.ArgumentParser() _PARSER.add_argument("root") diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 3aa93af..6370d53 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -3,6 +3,7 @@ import io import json import os +import shutil import subprocess import sys import tempfile @@ -200,6 +201,18 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): with self.subTest(private_path=private_path), self.assertRaises(ValueError): evidence.validate_evidence_receipt(value) + for identifier in ( + "/nix/store/private-checkout", + "model:/workspace/Skills/private", + "model@/workspace/Skills/private", + "model+/workspace/Skills/private", + "model_/workspace/Skills/private", + ): + value = copy.deepcopy(self.receipt) + value["run"]["model"] = identifier + with self.subTest(identifier=identifier), self.assertRaises(ValueError): + evidence.validate_evidence_receipt(value) + deeply_encoded_target = "/tmp/private/secret.txt" deeply_encoded_parameter = "guide.md?token=SUPERSECRET" for _ in range(4): @@ -407,6 +420,18 @@ def test_orientation_skips_heading_shaped_frontmatter(self): observed["frontmatter_title"], {"status": "unavailable", "value": None} ) + crlf_oversized = root / "crlf-oversized.md" + crlf_oversized.write_bytes( + b"---\r\n" + b"#\r\n" * 25_000 + b"---\r\n# Actual H1\r\n" + ) + observed = evidence.observe_entry_orientation(root, crlf_oversized.name) + self.assertEqual( + observed["literal_h1"], {"status": "unavailable", "value": None} + ) + self.assertEqual( + observed["frontmatter_title"], {"status": "unavailable", "value": None} + ) + mixed = root / "mixed.md" mixed.write_text( "---\ntitle: Proven title\ntags: [one, two]\n---\n\n## Start\n", @@ -449,6 +474,9 @@ def test_orientation_ignores_comments_and_indented_code(self): "mdx-inline": "{/* note */} # not a heading\n", "four-backticks": "````\n```\n# not a heading\n````\n", "mixed-fence": "```\n~~~\n# not a heading\n```\n", + "fence-inline-ticks": "```text\nsome ``` inline\n# not a heading\n```\n## Start\n", + "fence-indented-ticks": "```text\n ```\n# not a heading\n```\n## Start\n", + "fence-nbsp-close": "```text\n# not a heading\n```\u00a0\n# still not a heading\n```\n## Start\n", "actual-h1": "# Actual H1\n", "indented-html-opener": " \n## Start\n", - {"status": "completed", "value": False}, + {"status": "completed", "value": True}, ), "midline-mdx.mdx": ( "prefix {/*\n# not a heading\n*/}\n## Start\n", {"status": "completed", "value": False}, ), + "chained-html-comments.md": ( + " \n## Start\n", + {"status": "completed", "value": False}, + ), + "reopened-html-comment.md": ( + " prefix \n## Start\n", + {"status": "completed", "value": False}, + ), + "chained-mdx-comments.mdx": ( + "{/* one */} {/*\n# not a heading\n*/}\n## Start\n", + {"status": "completed", "value": False}, + ), + "reopened-mdx-comment.mdx": ( + "{/*\n*/} prefix {/*\n# not a heading\n*/}\n## Start\n", + {"status": "completed", "value": False}, + ), "raw-pre.md": ( "
\n# not a heading\n
\n## Start\n", {"status": "completed", "value": False}, ), "raw-script.mdx": ( "\n## Start\n", + {"status": "completed", "value": True}, + ), + "nbsp-div-boundary.md": ( + "\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "nbsp-script-boundary.md": ( + "\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "nbsp-script-close.md": ( + "\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-with-comment.md": ( + "
\n# not a heading\n
\n## Start\n", {"status": "completed", "value": False}, ), + "raw-div.md": ( + "
\n# not a heading\n
\n\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-div-nbsp-line.md": ( + "
\n\u00a0\n# not a heading\n", + {"status": "completed", "value": False}, + ), + "raw-table.mdx": ( + "\n# not a heading\n
\n\n## Start\n", + {"status": "completed", "value": True}, + ), + "mdx-lowercase-pre.mdx": ( + "
\n# Actual H1\n
\n", + {"status": "completed", "value": True}, + ), + "mdx-lowercase-style.mdx": ( + "\n", + {"status": "completed", "value": True}, + ), + "mdx-lowercase-textarea.mdx": ( + "\n", + {"status": "completed", "value": True}, + ), + "raw-base.md": ( + "\n# not a heading\n\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-basefont.md": ( + "\n# not a heading\n\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-processing.md": ( + "\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-cdata.md": ( + "\n## Start\n", + {"status": "completed", "value": False}, + ), + "raw-custom.md": ( + "\n# not a heading\n\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "raw-invalid-custom.md": ( + "\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "mdx-component-child.mdx": ( + "\n# Actual H1\n\n", + {"status": "completed", "value": True}, + ), + "mdx-capitalized-table.mdx": ( + "\n# Actual H1\n
\n", + {"status": "completed", "value": True}, + ), + "mdx-capitalized-script.mdx": ( + "\n", + {"status": "completed", "value": True}, + ), + "mdx-jsx-html-string.mdx": ( + '\n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "mdx-jsx-comment-string.mdx": ( + '\n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "mdx-lowercase-jsx-string.mdx": ( + '\n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "mdx-midline-jsx-string.mdx": ( + 'prefix \n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "mdx-jsx-quoted-html-marker.mdx": ( + 'prefix \n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "mdx-jsx-quoted-mdx-marker.mdx": ( + 'prefix \n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "markdown-quoted-tag-marker.md": ( + 'prefix x\n# Actual H1\n', + {"status": "completed", "value": True}, + ), "esm-template.mdx": ( "export const example = `\n# not a heading\n`\n", {"status": "unavailable", "value": None}, @@ -502,6 +650,214 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): "import Example from './example'\n\n# Actual H1\n", {"status": "completed", "value": True}, ), + "esm-import-comment-text.mdx": ( + "import Example from './'\n\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "esm-export-html-text.mdx": ( + 'export const marker = "\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "short-inline-html-dash.md": ( + "prefix \n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "short-block-html-dash.md": ( + "\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "multiline-mdx-expression.mdx": ( + "{`\n# not a heading\n`}\n", + {"status": "unavailable", "value": None}, + ), + "midline-mdx-expression.mdx": ( + "prefix {\n# not a heading\n}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "tag-then-html-comment.md": ( + "prefix \n## Start\n", + {"status": "completed", "value": True}, + ), + "jsx-then-mdx-comment.mdx": ( + "prefix {/*\n# not a heading\n*/}\n## Start\n", + {"status": "completed", "value": False}, + ), + "leading-jsx-then-mdx-comment.mdx": ( + " {/*\n# not a heading\n*/}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-jsx-then-html-comment.mdx": ( + " \n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-lower-jsx-then-mdx-comment.mdx": ( + " {/*\n# not a heading\n*/}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-lower-jsx-then-html-comment.mdx": ( + " \n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-lower-jsx-then-expression.mdx": ( + " {\n# not a heading\n}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-custom-element-then-comment.mdx": ( + " {/*\n# not a heading\n*/}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-quoted-lower-jsx-then-comment.mdx": ( + ' {/*\n# not a heading\n*/}\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "html-comment-then-expression.mdx": ( + "prefix {\n# not a heading\n}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "mdx-comment-then-expression.mdx": ( + "prefix {/* closed */} {\n# not a heading\n}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-html-comment-then-expression.mdx": ( + " {\n# not a heading\n}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-mdx-comment-then-expression.mdx": ( + "{/* closed */} {\n# not a heading\n}\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "leading-mdx-then-html-comment.mdx": ( + "{/* closed */} \n## Start\n", + {"status": "unavailable", "value": None}, + ), } with tempfile.TemporaryDirectory() as td: root = Path(td) @@ -512,6 +868,32 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): with self.subTest(name=name): self.assertEqual(observed["literal_h1"], expected) + def test_orientation_structural_lines_are_newline_style_independent(self): + scenarios = { + "empty-h1.md": ("#", {"status": "completed", "value": True}), + "type-one-raw.md": ( + "", + {"status": "completed", "value": False}, + ), + "type-six-raw.md": ( + " Date: Sat, 18 Jul 2026 05:50:06 -0400 Subject: [PATCH 08/20] Make MDX export scanning linear --- .../docs/scripts/_docs_checker/evidence.py | 3 ++- .../copilot/scripts/_docs_checker/evidence.py | 3 ++- .../cursor/scripts/_docs_checker/evidence.py | 3 ++- .../grok/scripts/_docs_checker/evidence.py | 3 ++- .../docs/scripts/_docs_checker/evidence.py | 3 ++- skills/docs/scripts/_docs_checker/evidence.py | 3 ++- tests/test_docs_evidence.py | 22 +++++++++++++++++++ 7 files changed, 34 insertions(+), 6 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 2c0fb67..7d444df 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -693,7 +693,8 @@ def _is_single_line_mdx_esm(line): stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" + r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) return simple_import is not None or simple_export is not None diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 2c0fb67..7d444df 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -693,7 +693,8 @@ def _is_single_line_mdx_esm(line): stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" + r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) return simple_import is not None or simple_export is not None diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 2c0fb67..7d444df 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -693,7 +693,8 @@ def _is_single_line_mdx_esm(line): stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" + r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) return simple_import is not None or simple_export is not None diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 2c0fb67..7d444df 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -693,7 +693,8 @@ def _is_single_line_mdx_esm(line): stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" + r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) return simple_import is not None or simple_export is not None diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 2c0fb67..7d444df 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -693,7 +693,8 @@ def _is_single_line_mdx_esm(line): stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" + r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) return simple_import is not None or simple_export is not None diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 2c0fb67..7d444df 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -693,7 +693,8 @@ def _is_single_line_mdx_esm(line): stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(['\"])(?:\\.|(?!\1).)*\1\s*;?", + r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" + r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) return simple_import is not None or simple_export is not None diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 6370d53..e3f7215 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -894,6 +894,28 @@ def test_orientation_structural_lines_are_newline_style_independent(self): with self.subTest(name=name, newline=repr(newline)): self.assertEqual(observed["literal_h1"], expected) + def test_orientation_export_string_scan_is_linear_and_inert(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + closed_entry = root / "long-export-closed.mdx" + closed_entry.write_text( + 'export const marker = "' + "\\a" * 50_000 + '"\n\n# Actual H1\n', + encoding="utf-8", + ) + closed = evidence.observe_entry_orientation(root, closed_entry.name) + open_entry = root / "long-export-open.mdx" + open_entry.write_text( + 'export const marker = "' + "\\a" * 50_000, + encoding="utf-8", + ) + open_string = evidence.observe_entry_orientation(root, open_entry.name) + self.assertEqual( + closed["literal_h1"], {"status": "completed", "value": True} + ) + self.assertEqual( + open_string["literal_h1"], {"status": "unavailable", "value": None} + ) + def test_stdout_receipt_entrypoint_combines_one_checker_run(self): with tempfile.TemporaryDirectory() as td: base = Path(td) From 893620ecd3a005f94a5d7113760e166368663373 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 05:58:06 -0400 Subject: [PATCH 09/20] Harden receipt route and title evidence --- .../docs/scripts/_docs_checker/evidence.py | 26 +++++++++++++------ .../copilot/scripts/_docs_checker/evidence.py | 26 +++++++++++++------ .../cursor/scripts/_docs_checker/evidence.py | 26 +++++++++++++------ .../grok/scripts/_docs_checker/evidence.py | 26 +++++++++++++------ .../docs/scripts/_docs_checker/evidence.py | 26 +++++++++++++------ skills/docs/scripts/_docs_checker/evidence.py | 26 +++++++++++++------ tests/test_docs_evidence.py | 18 +++++++++++++ 7 files changed, 126 insertions(+), 48 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 7d444df..85938af 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -241,6 +241,13 @@ def _route(value, name): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") + route_text = ( + candidate[1:] + if candidate.startswith("/") and not candidate.startswith("//") + else candidate + ) + if shared_text_exposes_route(route_text): + raise ValueError(f"{name} exposes a private or unsafe route") return value @@ -1055,15 +1062,18 @@ def observe_entry_orientation(root, entry): uncertain or in_mdx_esm or in_mdx_expression or inline_code_length ): literal_h1 = None - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) - title = metadata.get("values", {}).get("title") - unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: - frontmatter_title = evidence_value("completed", bool(title.strip())) - elif metadata.get("status") in {"absent", "measured"}: - frontmatter_title = evidence_value("completed", False) - else: + if body_lines is None: frontmatter_title = evidence_value("unavailable") + else: + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { "literal_h1": evidence_value("completed", literal_h1) if literal_h1 is not None diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 7d444df..85938af 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -241,6 +241,13 @@ def _route(value, name): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") + route_text = ( + candidate[1:] + if candidate.startswith("/") and not candidate.startswith("//") + else candidate + ) + if shared_text_exposes_route(route_text): + raise ValueError(f"{name} exposes a private or unsafe route") return value @@ -1055,15 +1062,18 @@ def observe_entry_orientation(root, entry): uncertain or in_mdx_esm or in_mdx_expression or inline_code_length ): literal_h1 = None - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) - title = metadata.get("values", {}).get("title") - unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: - frontmatter_title = evidence_value("completed", bool(title.strip())) - elif metadata.get("status") in {"absent", "measured"}: - frontmatter_title = evidence_value("completed", False) - else: + if body_lines is None: frontmatter_title = evidence_value("unavailable") + else: + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { "literal_h1": evidence_value("completed", literal_h1) if literal_h1 is not None diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 7d444df..85938af 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -241,6 +241,13 @@ def _route(value, name): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") + route_text = ( + candidate[1:] + if candidate.startswith("/") and not candidate.startswith("//") + else candidate + ) + if shared_text_exposes_route(route_text): + raise ValueError(f"{name} exposes a private or unsafe route") return value @@ -1055,15 +1062,18 @@ def observe_entry_orientation(root, entry): uncertain or in_mdx_esm or in_mdx_expression or inline_code_length ): literal_h1 = None - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) - title = metadata.get("values", {}).get("title") - unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: - frontmatter_title = evidence_value("completed", bool(title.strip())) - elif metadata.get("status") in {"absent", "measured"}: - frontmatter_title = evidence_value("completed", False) - else: + if body_lines is None: frontmatter_title = evidence_value("unavailable") + else: + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { "literal_h1": evidence_value("completed", literal_h1) if literal_h1 is not None diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 7d444df..85938af 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -241,6 +241,13 @@ def _route(value, name): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") + route_text = ( + candidate[1:] + if candidate.startswith("/") and not candidate.startswith("//") + else candidate + ) + if shared_text_exposes_route(route_text): + raise ValueError(f"{name} exposes a private or unsafe route") return value @@ -1055,15 +1062,18 @@ def observe_entry_orientation(root, entry): uncertain or in_mdx_esm or in_mdx_expression or inline_code_length ): literal_h1 = None - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) - title = metadata.get("values", {}).get("title") - unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: - frontmatter_title = evidence_value("completed", bool(title.strip())) - elif metadata.get("status") in {"absent", "measured"}: - frontmatter_title = evidence_value("completed", False) - else: + if body_lines is None: frontmatter_title = evidence_value("unavailable") + else: + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { "literal_h1": evidence_value("completed", literal_h1) if literal_h1 is not None diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 7d444df..85938af 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -241,6 +241,13 @@ def _route(value, name): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") + route_text = ( + candidate[1:] + if candidate.startswith("/") and not candidate.startswith("//") + else candidate + ) + if shared_text_exposes_route(route_text): + raise ValueError(f"{name} exposes a private or unsafe route") return value @@ -1055,15 +1062,18 @@ def observe_entry_orientation(root, entry): uncertain or in_mdx_esm or in_mdx_expression or inline_code_length ): literal_h1 = None - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) - title = metadata.get("values", {}).get("title") - unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: - frontmatter_title = evidence_value("completed", bool(title.strip())) - elif metadata.get("status") in {"absent", "measured"}: - frontmatter_title = evidence_value("completed", False) - else: + if body_lines is None: frontmatter_title = evidence_value("unavailable") + else: + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { "literal_h1": evidence_value("completed", literal_h1) if literal_h1 is not None diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 7d444df..85938af 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -241,6 +241,13 @@ def _route(value, name): raise ValueError(f"{name} must be a local route") if _CREDENTIAL_PARAMETER.search(current): raise ValueError(f"{name} exposes credential-shaped data") + route_text = ( + candidate[1:] + if candidate.startswith("/") and not candidate.startswith("//") + else candidate + ) + if shared_text_exposes_route(route_text): + raise ValueError(f"{name} exposes a private or unsafe route") return value @@ -1055,15 +1062,18 @@ def observe_entry_orientation(root, entry): uncertain or in_mdx_esm or in_mdx_expression or inline_code_length ): literal_h1 = None - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) - title = metadata.get("values", {}).get("title") - unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: - frontmatter_title = evidence_value("completed", bool(title.strip())) - elif metadata.get("status") in {"absent", "measured"}: - frontmatter_title = evidence_value("completed", False) - else: + if body_lines is None: frontmatter_title = evidence_value("unavailable") + else: + metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + title = metadata.get("values", {}).get("title") + unresolved_metadata = set(metadata.get("unresolved", ())) + if isinstance(title, str) and "title" not in unresolved_metadata: + frontmatter_title = evidence_value("completed", bool(title.strip())) + elif metadata.get("status") in {"absent", "measured"}: + frontmatter_title = evidence_value("completed", False) + else: + frontmatter_title = evidence_value("unavailable") return { "literal_h1": evidence_value("completed", literal_h1) if literal_h1 is not None diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index e3f7215..097be29 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -127,6 +127,10 @@ def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): "/tmp/private/secret.txt", "/root/.ssh/id_rsa", r"\\server\private\secret.txt", + "../private-roadmap.md", + "..%2fprivate-roadmap.md", + "%2e%2e/private.md", + "/docs/../private-roadmap.md", "see /tmp/private/secret.txt", "file:///etc/passwd", "guide.md?token=SUPERSECRET", @@ -409,6 +413,20 @@ def test_orientation_skips_heading_shaped_frontmatter(self): observed["frontmatter_title"], {"status": "unavailable", "value": None} ) + scalar_close = root / "scalar-close.md" + scalar_close.write_text( + "---\ntitle: Guide\ndescription: |\n ---\n# still scalar\n", + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, scalar_close.name) + self.assertEqual( + observed["literal_h1"], {"status": "unavailable", "value": None} + ) + self.assertEqual( + observed["frontmatter_title"], + {"status": "unavailable", "value": None}, + ) + oversized = root / "oversized.md" oversized.write_text( "---\ntitle: Guide\n" + "x" * evidence.MAX_FRONTMATTER_BYTES, From a63822db4ad122d60580314c93ecbd0c0ebba259 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 06:19:08 -0400 Subject: [PATCH 10/20] Fail closed on dirty receipt audits --- .../docs/scripts/_docs_checker/evidence.py | 12 +++++- .../skills/docs/scripts/evidence_receipt.py | 6 ++- .../copilot/scripts/_docs_checker/evidence.py | 12 +++++- adapters/copilot/scripts/evidence_receipt.py | 6 ++- .../cursor/scripts/_docs_checker/evidence.py | 12 +++++- adapters/cursor/scripts/evidence_receipt.py | 6 ++- .../grok/scripts/_docs_checker/evidence.py | 12 +++++- adapters/grok/scripts/evidence_receipt.py | 6 ++- .../docs/scripts/_docs_checker/evidence.py | 12 +++++- .../skills/docs/scripts/evidence_receipt.py | 6 ++- skills/docs/scripts/_docs_checker/evidence.py | 12 +++++- skills/docs/scripts/evidence_receipt.py | 6 ++- tests/test_docs_evidence.py | 38 +++++++++++++++++++ 13 files changed, 128 insertions(+), 18 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 85938af..420a95c 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -481,6 +481,12 @@ def validate_evidence_receipt(value): _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + audit_completed = all( + write_audit[field]["status"] == "completed" + for field in ("writes_attempted", "writes_observed") + ) + if (write_audit["status"] == "completed") != audit_completed: + raise ValueError("write_audit.status does not match its evidence") git = _exact_keys(value["git"], {"before", "after"}, "git") for field in git: @@ -1250,9 +1256,11 @@ def build_evidence_receipt( }, "doctor": dict(doctor), "write_audit": { - "status": "completed", + "status": "completed" if writes_observed is not None else "unavailable", "writes_attempted": evidence_value("completed", writes_attempted), - "writes_observed": evidence_value("completed", writes_observed), + "writes_observed": evidence_value("completed", writes_observed) + if writes_observed is not None + else evidence_value("unavailable"), }, "git": { "before": evidence_value("completed", git_before), diff --git a/adapters/claude/skills/docs/scripts/evidence_receipt.py b/adapters/claude/skills/docs/scripts/evidence_receipt.py index 555bd09..d251f82 100644 --- a/adapters/claude/skills/docs/scripts/evidence_receipt.py +++ b/adapters/claude/skills/docs/scripts/evidence_receipt.py @@ -112,7 +112,11 @@ def main(argv=None): unresolved=metadata["unresolved"], doctor=metadata["doctor"], writes_attempted=0, - writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + writes_observed=( + None + if before_raw + else 0 if before_raw == after_raw else len(after_raw.splitlines()) + ), git_before=before, git_after=after, ) diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 85938af..420a95c 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -481,6 +481,12 @@ def validate_evidence_receipt(value): _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + audit_completed = all( + write_audit[field]["status"] == "completed" + for field in ("writes_attempted", "writes_observed") + ) + if (write_audit["status"] == "completed") != audit_completed: + raise ValueError("write_audit.status does not match its evidence") git = _exact_keys(value["git"], {"before", "after"}, "git") for field in git: @@ -1250,9 +1256,11 @@ def build_evidence_receipt( }, "doctor": dict(doctor), "write_audit": { - "status": "completed", + "status": "completed" if writes_observed is not None else "unavailable", "writes_attempted": evidence_value("completed", writes_attempted), - "writes_observed": evidence_value("completed", writes_observed), + "writes_observed": evidence_value("completed", writes_observed) + if writes_observed is not None + else evidence_value("unavailable"), }, "git": { "before": evidence_value("completed", git_before), diff --git a/adapters/copilot/scripts/evidence_receipt.py b/adapters/copilot/scripts/evidence_receipt.py index 555bd09..d251f82 100644 --- a/adapters/copilot/scripts/evidence_receipt.py +++ b/adapters/copilot/scripts/evidence_receipt.py @@ -112,7 +112,11 @@ def main(argv=None): unresolved=metadata["unresolved"], doctor=metadata["doctor"], writes_attempted=0, - writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + writes_observed=( + None + if before_raw + else 0 if before_raw == after_raw else len(after_raw.splitlines()) + ), git_before=before, git_after=after, ) diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 85938af..420a95c 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -481,6 +481,12 @@ def validate_evidence_receipt(value): _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + audit_completed = all( + write_audit[field]["status"] == "completed" + for field in ("writes_attempted", "writes_observed") + ) + if (write_audit["status"] == "completed") != audit_completed: + raise ValueError("write_audit.status does not match its evidence") git = _exact_keys(value["git"], {"before", "after"}, "git") for field in git: @@ -1250,9 +1256,11 @@ def build_evidence_receipt( }, "doctor": dict(doctor), "write_audit": { - "status": "completed", + "status": "completed" if writes_observed is not None else "unavailable", "writes_attempted": evidence_value("completed", writes_attempted), - "writes_observed": evidence_value("completed", writes_observed), + "writes_observed": evidence_value("completed", writes_observed) + if writes_observed is not None + else evidence_value("unavailable"), }, "git": { "before": evidence_value("completed", git_before), diff --git a/adapters/cursor/scripts/evidence_receipt.py b/adapters/cursor/scripts/evidence_receipt.py index 555bd09..d251f82 100644 --- a/adapters/cursor/scripts/evidence_receipt.py +++ b/adapters/cursor/scripts/evidence_receipt.py @@ -112,7 +112,11 @@ def main(argv=None): unresolved=metadata["unresolved"], doctor=metadata["doctor"], writes_attempted=0, - writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + writes_observed=( + None + if before_raw + else 0 if before_raw == after_raw else len(after_raw.splitlines()) + ), git_before=before, git_after=after, ) diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 85938af..420a95c 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -481,6 +481,12 @@ def validate_evidence_receipt(value): _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + audit_completed = all( + write_audit[field]["status"] == "completed" + for field in ("writes_attempted", "writes_observed") + ) + if (write_audit["status"] == "completed") != audit_completed: + raise ValueError("write_audit.status does not match its evidence") git = _exact_keys(value["git"], {"before", "after"}, "git") for field in git: @@ -1250,9 +1256,11 @@ def build_evidence_receipt( }, "doctor": dict(doctor), "write_audit": { - "status": "completed", + "status": "completed" if writes_observed is not None else "unavailable", "writes_attempted": evidence_value("completed", writes_attempted), - "writes_observed": evidence_value("completed", writes_observed), + "writes_observed": evidence_value("completed", writes_observed) + if writes_observed is not None + else evidence_value("unavailable"), }, "git": { "before": evidence_value("completed", git_before), diff --git a/adapters/grok/scripts/evidence_receipt.py b/adapters/grok/scripts/evidence_receipt.py index 555bd09..d251f82 100644 --- a/adapters/grok/scripts/evidence_receipt.py +++ b/adapters/grok/scripts/evidence_receipt.py @@ -112,7 +112,11 @@ def main(argv=None): unresolved=metadata["unresolved"], doctor=metadata["doctor"], writes_attempted=0, - writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + writes_observed=( + None + if before_raw + else 0 if before_raw == after_raw else len(after_raw.splitlines()) + ), git_before=before, git_after=after, ) diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 85938af..420a95c 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -481,6 +481,12 @@ def validate_evidence_receipt(value): _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + audit_completed = all( + write_audit[field]["status"] == "completed" + for field in ("writes_attempted", "writes_observed") + ) + if (write_audit["status"] == "completed") != audit_completed: + raise ValueError("write_audit.status does not match its evidence") git = _exact_keys(value["git"], {"before", "after"}, "git") for field in git: @@ -1250,9 +1256,11 @@ def build_evidence_receipt( }, "doctor": dict(doctor), "write_audit": { - "status": "completed", + "status": "completed" if writes_observed is not None else "unavailable", "writes_attempted": evidence_value("completed", writes_attempted), - "writes_observed": evidence_value("completed", writes_observed), + "writes_observed": evidence_value("completed", writes_observed) + if writes_observed is not None + else evidence_value("unavailable"), }, "git": { "before": evidence_value("completed", git_before), diff --git a/adapters/plugin/skills/docs/scripts/evidence_receipt.py b/adapters/plugin/skills/docs/scripts/evidence_receipt.py index 555bd09..d251f82 100644 --- a/adapters/plugin/skills/docs/scripts/evidence_receipt.py +++ b/adapters/plugin/skills/docs/scripts/evidence_receipt.py @@ -112,7 +112,11 @@ def main(argv=None): unresolved=metadata["unresolved"], doctor=metadata["doctor"], writes_attempted=0, - writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + writes_observed=( + None + if before_raw + else 0 if before_raw == after_raw else len(after_raw.splitlines()) + ), git_before=before, git_after=after, ) diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 85938af..420a95c 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -481,6 +481,12 @@ def validate_evidence_receipt(value): _enum_text(write_audit["status"], EVIDENCE_STATES, "write_audit.status") _evidence(write_audit["writes_attempted"], "write_audit.writes_attempted", validator=_integer) _evidence(write_audit["writes_observed"], "write_audit.writes_observed", validator=_integer) + audit_completed = all( + write_audit[field]["status"] == "completed" + for field in ("writes_attempted", "writes_observed") + ) + if (write_audit["status"] == "completed") != audit_completed: + raise ValueError("write_audit.status does not match its evidence") git = _exact_keys(value["git"], {"before", "after"}, "git") for field in git: @@ -1250,9 +1256,11 @@ def build_evidence_receipt( }, "doctor": dict(doctor), "write_audit": { - "status": "completed", + "status": "completed" if writes_observed is not None else "unavailable", "writes_attempted": evidence_value("completed", writes_attempted), - "writes_observed": evidence_value("completed", writes_observed), + "writes_observed": evidence_value("completed", writes_observed) + if writes_observed is not None + else evidence_value("unavailable"), }, "git": { "before": evidence_value("completed", git_before), diff --git a/skills/docs/scripts/evidence_receipt.py b/skills/docs/scripts/evidence_receipt.py index 555bd09..d251f82 100644 --- a/skills/docs/scripts/evidence_receipt.py +++ b/skills/docs/scripts/evidence_receipt.py @@ -112,7 +112,11 @@ def main(argv=None): unresolved=metadata["unresolved"], doctor=metadata["doctor"], writes_attempted=0, - writes_observed=0 if before_raw == after_raw else len(after_raw.splitlines()), + writes_observed=( + None + if before_raw + else 0 if before_raw == after_raw else len(after_raw.splitlines()) + ), git_before=before, git_after=after, ) diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 097be29..10e5f89 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -668,6 +668,14 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): "import Example from './example'\n\n# Actual H1\n", {"status": "completed", "value": True}, ), + "esm-named-import-parked.mdx": ( + "import { Callout } from './components'\n\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "esm-named-import-no-boundary.mdx": ( + "import { Callout } from './components'\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), "esm-import-comment-text.mdx": ( "import Example from './'\n\n# Actual H1\n", {"status": "completed", "value": True}, @@ -987,11 +995,41 @@ def test_stdout_receipt_entrypoint_combines_one_checker_run(self): capture_output=True, text=True, ) + (docs / "README.md").write_text( + "# Dirty before receipt\n\n## Start\n", encoding="utf-8" + ) + dirty_start = subprocess.run( + [ + sys.executable, + "-B", + str(EVIDENCE_CLI), + str(root), + "--metadata-file", + str(metadata), + ], + cwd=ROOT, + capture_output=True, + text=True, + ) self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr) receipt = json.loads(completed.stdout) evidence.validate_evidence_receipt(receipt) self.assertEqual(receipt["health"]["status"], "completed") self.assertEqual(receipt["write_audit"]["writes_observed"]["value"], 0) + self.assertEqual( + dirty_start.returncode, 0, dirty_start.stdout + dirty_start.stderr + ) + dirty_receipt = json.loads(dirty_start.stdout) + evidence.validate_evidence_receipt(dirty_receipt) + self.assertEqual(dirty_receipt["git"]["before"]["value"], "dirty") + self.assertEqual(dirty_receipt["write_audit"]["status"], "unavailable") + self.assertEqual( + dirty_receipt["write_audit"]["writes_observed"], + {"status": "unavailable", "value": None}, + ) + self.assertIn( + "write_audit.writes_observed", dirty_receipt["unavailable_evidence"] + ) def test_receipt_entrypoint_disables_bytecode_before_checker_imports(self): with tempfile.TemporaryDirectory() as td: From 66f34149104f110acbf9f4819b4cf132be6e236e Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 06:49:49 -0400 Subject: [PATCH 11/20] Align frontmatter and MDX ESM evidence --- .../docs/scripts/_docs_checker/evidence.py | 124 ++++++++++++++++-- .../docs/scripts/_docs_checker/formats.py | 12 +- .../copilot/scripts/_docs_checker/evidence.py | 124 ++++++++++++++++-- .../copilot/scripts/_docs_checker/formats.py | 12 +- .../cursor/scripts/_docs_checker/evidence.py | 124 ++++++++++++++++-- .../cursor/scripts/_docs_checker/formats.py | 12 +- .../grok/scripts/_docs_checker/evidence.py | 124 ++++++++++++++++-- .../grok/scripts/_docs_checker/formats.py | 12 +- .../docs/scripts/_docs_checker/evidence.py | 124 ++++++++++++++++-- .../docs/scripts/_docs_checker/formats.py | 12 +- skills/docs/scripts/_docs_checker/evidence.py | 124 ++++++++++++++++-- skills/docs/scripts/_docs_checker/formats.py | 12 +- tests/test_docs_evidence.py | 75 +++++++++++ 13 files changed, 831 insertions(+), 60 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 420a95c..551e1cd 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -10,6 +10,7 @@ import json import math import re +import string from collections.abc import Mapping, Sequence from pathlib import Path from urllib.parse import unquote @@ -543,6 +544,15 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _has_column_zero_frontmatter_opener(text): + source = text.removeprefix("\ufeff") + first_line = re.split(r"\r\n|\r|\n", source, maxsplit=1)[0] + return ( + not first_line.startswith((" ", "\t")) + and first_line.rstrip(" \t") == "---" + ) + + def _markdown_body_lines(text): """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" source = text.removeprefix("\ufeff") @@ -553,7 +563,7 @@ def _markdown_body_lines(text): start = match.end() if start < len(source): lines.append(source[start:]) - if not lines or lines[0].strip() != "---": + if not lines or not _has_column_zero_frontmatter_opener(text): return lines region_bytes = 0 for index, line in enumerate(lines): @@ -699,18 +709,96 @@ def _without_inline_code_spans(line, open_length=0): return "".join(masked), 0 +def _is_safe_single_line_js_string(value): + """Validate the bounded JavaScript string forms accepted as inert ESM.""" + if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]: + return False + content = value[1:-1] + index = 0 + while index < len(content): + char = content[index] + if char in "\r\n\u2028\u2029": + return False + if char != "\\": + index += 1 + continue + if index + 1 >= len(content): + return False + escaped = content[index + 1] + if escaped in "123456789": + return False + if escaped == "0": + if index + 2 < len(content) and content[index + 2].isdigit(): + return False + index += 2 + continue + if escaped == "x": + digits = content[index + 2 : index + 4] + if len(digits) != 2 or any(char not in string.hexdigits for char in digits): + return False + index += 4 + continue + if escaped == "u": + if index + 2 < len(content) and content[index + 2] == "{": + end = content.find("}", index + 3) + digits = content[index + 3 : end] if end >= 0 else "" + if ( + not 1 <= len(digits) <= 6 + or any(char not in string.hexdigits for char in digits) + or int(digits, 16) > 0x10FFFF + ): + return False + index = end + 1 + continue + digits = content[index + 2 : index + 6] + if len(digits) != 4 or any(char not in string.hexdigits for char in digits): + return False + index += 6 + continue + index += 2 + return True + + def _is_single_line_mdx_esm(line): stripped = line.strip() simple_import = re.fullmatch( - r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])+\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])+')\s*;?", stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" - r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", + r"export\s+const\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) - return simple_import is not None or simple_export is not None + binding = None + value = None + if simple_import is not None: + binding = simple_import.group("import_binding") + value = simple_import.group("import_value") + elif simple_export is not None: + binding = simple_export.group("export_binding") + value = simple_export.group("export_value") + else: + return False + return ( + (binding is None or binding not in _JS_RESERVED_BINDINGS) + and _is_safe_single_line_js_string(value) + ) + + +_JS_RESERVED_BINDINGS = frozenset( + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", + "false", "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", "protected", + "public", "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield", + } +) _RAW_HTML_BLANK_TAGS = frozenset( @@ -776,6 +864,7 @@ def observe_entry_orientation(root, entry): "provider_rendered_title": evidence_value("unavailable"), } body_lines = _markdown_body_lines(text) + frontmatter_opened = _has_column_zero_frontmatter_opener(text) literal_h1 = None if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" @@ -788,11 +877,23 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False + simple_mdx_esm_pending = False in_mdx_expression = False uncertain = False literal_h1 = False for raw_line in body_lines: line = raw_line.rstrip("\r\n") + if simple_mdx_esm_pending: + if _is_ascii_blank_line(line): + simple_mdx_esm_pending = False + continue + if re.match( + r"^(?:import|export)(?:\s|\{|$)", line + ) and _is_single_line_mdx_esm(line): + continue + uncertain = True + in_mdx_esm = True + continue if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -901,8 +1002,11 @@ def observe_entry_orientation(root, entry): if component_document and re.match( r"^(?:import|export)(?:\s|\{|$)", pre_stripped ): - uncertain = True - in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + if _is_single_line_mdx_esm(pre_stripped): + simple_mdx_esm_pending = True + else: + uncertain = True + in_mdx_esm = True continue jsx_match = re.match( r"^|$)", pre_stripped @@ -1071,7 +1175,11 @@ def observe_entry_orientation(root, entry): if body_lines is None: frontmatter_title = evidence_value("unavailable") else: - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + metadata = ( + parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + if frontmatter_opened + else {"status": "absent", "values": {}, "unresolved": []} + ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) if isinstance(title, str) and "title" not in unresolved_metadata: diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/formats.py b/adapters/claude/skills/docs/scripts/_docs_checker/formats.py index d83fff6..ea51f3a 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/formats.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/formats.py @@ -12,6 +12,14 @@ _FRONTMATTER_KEY = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$") +def _is_column_zero_frontmatter_delimiter(line, allowed): + delimiter = line.rstrip("\r\n") + return ( + not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in allowed + ) + + def is_document_path(value): """Return whether a path is supported as inert documentation text.""" return Path(value).suffix.casefold() in DOCUMENT_SUFFIXES @@ -41,7 +49,7 @@ def parse_frontmatter_scalars(text): if not isinstance(text, str): return {"status": "unresolved", "values": {}, "unresolved": ["document"]} lines = text.removeprefix("\ufeff").splitlines(keepends=True) - if not lines or lines[0].strip() != "---": + if not lines or not _is_column_zero_frontmatter_delimiter(lines[0], {"---"}): return {"status": "absent", "values": {}, "unresolved": []} region_bytes = 0 closing = None @@ -49,7 +57,7 @@ def parse_frontmatter_scalars(text): region_bytes += len(line.encode("utf-8", "strict")) if region_bytes > MAX_FRONTMATTER_BYTES: return {"status": "unresolved", "values": {}, "unresolved": ["size"]} - if index and line.strip() in {"---", "..."}: + if index and _is_column_zero_frontmatter_delimiter(line, {"---", "..."}): closing = index break if closing is None: diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 420a95c..551e1cd 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -10,6 +10,7 @@ import json import math import re +import string from collections.abc import Mapping, Sequence from pathlib import Path from urllib.parse import unquote @@ -543,6 +544,15 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _has_column_zero_frontmatter_opener(text): + source = text.removeprefix("\ufeff") + first_line = re.split(r"\r\n|\r|\n", source, maxsplit=1)[0] + return ( + not first_line.startswith((" ", "\t")) + and first_line.rstrip(" \t") == "---" + ) + + def _markdown_body_lines(text): """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" source = text.removeprefix("\ufeff") @@ -553,7 +563,7 @@ def _markdown_body_lines(text): start = match.end() if start < len(source): lines.append(source[start:]) - if not lines or lines[0].strip() != "---": + if not lines or not _has_column_zero_frontmatter_opener(text): return lines region_bytes = 0 for index, line in enumerate(lines): @@ -699,18 +709,96 @@ def _without_inline_code_spans(line, open_length=0): return "".join(masked), 0 +def _is_safe_single_line_js_string(value): + """Validate the bounded JavaScript string forms accepted as inert ESM.""" + if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]: + return False + content = value[1:-1] + index = 0 + while index < len(content): + char = content[index] + if char in "\r\n\u2028\u2029": + return False + if char != "\\": + index += 1 + continue + if index + 1 >= len(content): + return False + escaped = content[index + 1] + if escaped in "123456789": + return False + if escaped == "0": + if index + 2 < len(content) and content[index + 2].isdigit(): + return False + index += 2 + continue + if escaped == "x": + digits = content[index + 2 : index + 4] + if len(digits) != 2 or any(char not in string.hexdigits for char in digits): + return False + index += 4 + continue + if escaped == "u": + if index + 2 < len(content) and content[index + 2] == "{": + end = content.find("}", index + 3) + digits = content[index + 3 : end] if end >= 0 else "" + if ( + not 1 <= len(digits) <= 6 + or any(char not in string.hexdigits for char in digits) + or int(digits, 16) > 0x10FFFF + ): + return False + index = end + 1 + continue + digits = content[index + 2 : index + 6] + if len(digits) != 4 or any(char not in string.hexdigits for char in digits): + return False + index += 6 + continue + index += 2 + return True + + def _is_single_line_mdx_esm(line): stripped = line.strip() simple_import = re.fullmatch( - r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])+\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])+')\s*;?", stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" - r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", + r"export\s+const\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) - return simple_import is not None or simple_export is not None + binding = None + value = None + if simple_import is not None: + binding = simple_import.group("import_binding") + value = simple_import.group("import_value") + elif simple_export is not None: + binding = simple_export.group("export_binding") + value = simple_export.group("export_value") + else: + return False + return ( + (binding is None or binding not in _JS_RESERVED_BINDINGS) + and _is_safe_single_line_js_string(value) + ) + + +_JS_RESERVED_BINDINGS = frozenset( + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", + "false", "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", "protected", + "public", "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield", + } +) _RAW_HTML_BLANK_TAGS = frozenset( @@ -776,6 +864,7 @@ def observe_entry_orientation(root, entry): "provider_rendered_title": evidence_value("unavailable"), } body_lines = _markdown_body_lines(text) + frontmatter_opened = _has_column_zero_frontmatter_opener(text) literal_h1 = None if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" @@ -788,11 +877,23 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False + simple_mdx_esm_pending = False in_mdx_expression = False uncertain = False literal_h1 = False for raw_line in body_lines: line = raw_line.rstrip("\r\n") + if simple_mdx_esm_pending: + if _is_ascii_blank_line(line): + simple_mdx_esm_pending = False + continue + if re.match( + r"^(?:import|export)(?:\s|\{|$)", line + ) and _is_single_line_mdx_esm(line): + continue + uncertain = True + in_mdx_esm = True + continue if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -901,8 +1002,11 @@ def observe_entry_orientation(root, entry): if component_document and re.match( r"^(?:import|export)(?:\s|\{|$)", pre_stripped ): - uncertain = True - in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + if _is_single_line_mdx_esm(pre_stripped): + simple_mdx_esm_pending = True + else: + uncertain = True + in_mdx_esm = True continue jsx_match = re.match( r"^|$)", pre_stripped @@ -1071,7 +1175,11 @@ def observe_entry_orientation(root, entry): if body_lines is None: frontmatter_title = evidence_value("unavailable") else: - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + metadata = ( + parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + if frontmatter_opened + else {"status": "absent", "values": {}, "unresolved": []} + ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) if isinstance(title, str) and "title" not in unresolved_metadata: diff --git a/adapters/copilot/scripts/_docs_checker/formats.py b/adapters/copilot/scripts/_docs_checker/formats.py index d83fff6..ea51f3a 100644 --- a/adapters/copilot/scripts/_docs_checker/formats.py +++ b/adapters/copilot/scripts/_docs_checker/formats.py @@ -12,6 +12,14 @@ _FRONTMATTER_KEY = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$") +def _is_column_zero_frontmatter_delimiter(line, allowed): + delimiter = line.rstrip("\r\n") + return ( + not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in allowed + ) + + def is_document_path(value): """Return whether a path is supported as inert documentation text.""" return Path(value).suffix.casefold() in DOCUMENT_SUFFIXES @@ -41,7 +49,7 @@ def parse_frontmatter_scalars(text): if not isinstance(text, str): return {"status": "unresolved", "values": {}, "unresolved": ["document"]} lines = text.removeprefix("\ufeff").splitlines(keepends=True) - if not lines or lines[0].strip() != "---": + if not lines or not _is_column_zero_frontmatter_delimiter(lines[0], {"---"}): return {"status": "absent", "values": {}, "unresolved": []} region_bytes = 0 closing = None @@ -49,7 +57,7 @@ def parse_frontmatter_scalars(text): region_bytes += len(line.encode("utf-8", "strict")) if region_bytes > MAX_FRONTMATTER_BYTES: return {"status": "unresolved", "values": {}, "unresolved": ["size"]} - if index and line.strip() in {"---", "..."}: + if index and _is_column_zero_frontmatter_delimiter(line, {"---", "..."}): closing = index break if closing is None: diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 420a95c..551e1cd 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -10,6 +10,7 @@ import json import math import re +import string from collections.abc import Mapping, Sequence from pathlib import Path from urllib.parse import unquote @@ -543,6 +544,15 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _has_column_zero_frontmatter_opener(text): + source = text.removeprefix("\ufeff") + first_line = re.split(r"\r\n|\r|\n", source, maxsplit=1)[0] + return ( + not first_line.startswith((" ", "\t")) + and first_line.rstrip(" \t") == "---" + ) + + def _markdown_body_lines(text): """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" source = text.removeprefix("\ufeff") @@ -553,7 +563,7 @@ def _markdown_body_lines(text): start = match.end() if start < len(source): lines.append(source[start:]) - if not lines or lines[0].strip() != "---": + if not lines or not _has_column_zero_frontmatter_opener(text): return lines region_bytes = 0 for index, line in enumerate(lines): @@ -699,18 +709,96 @@ def _without_inline_code_spans(line, open_length=0): return "".join(masked), 0 +def _is_safe_single_line_js_string(value): + """Validate the bounded JavaScript string forms accepted as inert ESM.""" + if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]: + return False + content = value[1:-1] + index = 0 + while index < len(content): + char = content[index] + if char in "\r\n\u2028\u2029": + return False + if char != "\\": + index += 1 + continue + if index + 1 >= len(content): + return False + escaped = content[index + 1] + if escaped in "123456789": + return False + if escaped == "0": + if index + 2 < len(content) and content[index + 2].isdigit(): + return False + index += 2 + continue + if escaped == "x": + digits = content[index + 2 : index + 4] + if len(digits) != 2 or any(char not in string.hexdigits for char in digits): + return False + index += 4 + continue + if escaped == "u": + if index + 2 < len(content) and content[index + 2] == "{": + end = content.find("}", index + 3) + digits = content[index + 3 : end] if end >= 0 else "" + if ( + not 1 <= len(digits) <= 6 + or any(char not in string.hexdigits for char in digits) + or int(digits, 16) > 0x10FFFF + ): + return False + index = end + 1 + continue + digits = content[index + 2 : index + 6] + if len(digits) != 4 or any(char not in string.hexdigits for char in digits): + return False + index += 6 + continue + index += 2 + return True + + def _is_single_line_mdx_esm(line): stripped = line.strip() simple_import = re.fullmatch( - r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])+\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])+')\s*;?", stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" - r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", + r"export\s+const\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) - return simple_import is not None or simple_export is not None + binding = None + value = None + if simple_import is not None: + binding = simple_import.group("import_binding") + value = simple_import.group("import_value") + elif simple_export is not None: + binding = simple_export.group("export_binding") + value = simple_export.group("export_value") + else: + return False + return ( + (binding is None or binding not in _JS_RESERVED_BINDINGS) + and _is_safe_single_line_js_string(value) + ) + + +_JS_RESERVED_BINDINGS = frozenset( + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", + "false", "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", "protected", + "public", "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield", + } +) _RAW_HTML_BLANK_TAGS = frozenset( @@ -776,6 +864,7 @@ def observe_entry_orientation(root, entry): "provider_rendered_title": evidence_value("unavailable"), } body_lines = _markdown_body_lines(text) + frontmatter_opened = _has_column_zero_frontmatter_opener(text) literal_h1 = None if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" @@ -788,11 +877,23 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False + simple_mdx_esm_pending = False in_mdx_expression = False uncertain = False literal_h1 = False for raw_line in body_lines: line = raw_line.rstrip("\r\n") + if simple_mdx_esm_pending: + if _is_ascii_blank_line(line): + simple_mdx_esm_pending = False + continue + if re.match( + r"^(?:import|export)(?:\s|\{|$)", line + ) and _is_single_line_mdx_esm(line): + continue + uncertain = True + in_mdx_esm = True + continue if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -901,8 +1002,11 @@ def observe_entry_orientation(root, entry): if component_document and re.match( r"^(?:import|export)(?:\s|\{|$)", pre_stripped ): - uncertain = True - in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + if _is_single_line_mdx_esm(pre_stripped): + simple_mdx_esm_pending = True + else: + uncertain = True + in_mdx_esm = True continue jsx_match = re.match( r"^|$)", pre_stripped @@ -1071,7 +1175,11 @@ def observe_entry_orientation(root, entry): if body_lines is None: frontmatter_title = evidence_value("unavailable") else: - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + metadata = ( + parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + if frontmatter_opened + else {"status": "absent", "values": {}, "unresolved": []} + ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) if isinstance(title, str) and "title" not in unresolved_metadata: diff --git a/adapters/cursor/scripts/_docs_checker/formats.py b/adapters/cursor/scripts/_docs_checker/formats.py index d83fff6..ea51f3a 100644 --- a/adapters/cursor/scripts/_docs_checker/formats.py +++ b/adapters/cursor/scripts/_docs_checker/formats.py @@ -12,6 +12,14 @@ _FRONTMATTER_KEY = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$") +def _is_column_zero_frontmatter_delimiter(line, allowed): + delimiter = line.rstrip("\r\n") + return ( + not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in allowed + ) + + def is_document_path(value): """Return whether a path is supported as inert documentation text.""" return Path(value).suffix.casefold() in DOCUMENT_SUFFIXES @@ -41,7 +49,7 @@ def parse_frontmatter_scalars(text): if not isinstance(text, str): return {"status": "unresolved", "values": {}, "unresolved": ["document"]} lines = text.removeprefix("\ufeff").splitlines(keepends=True) - if not lines or lines[0].strip() != "---": + if not lines or not _is_column_zero_frontmatter_delimiter(lines[0], {"---"}): return {"status": "absent", "values": {}, "unresolved": []} region_bytes = 0 closing = None @@ -49,7 +57,7 @@ def parse_frontmatter_scalars(text): region_bytes += len(line.encode("utf-8", "strict")) if region_bytes > MAX_FRONTMATTER_BYTES: return {"status": "unresolved", "values": {}, "unresolved": ["size"]} - if index and line.strip() in {"---", "..."}: + if index and _is_column_zero_frontmatter_delimiter(line, {"---", "..."}): closing = index break if closing is None: diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 420a95c..551e1cd 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -10,6 +10,7 @@ import json import math import re +import string from collections.abc import Mapping, Sequence from pathlib import Path from urllib.parse import unquote @@ -543,6 +544,15 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _has_column_zero_frontmatter_opener(text): + source = text.removeprefix("\ufeff") + first_line = re.split(r"\r\n|\r|\n", source, maxsplit=1)[0] + return ( + not first_line.startswith((" ", "\t")) + and first_line.rstrip(" \t") == "---" + ) + + def _markdown_body_lines(text): """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" source = text.removeprefix("\ufeff") @@ -553,7 +563,7 @@ def _markdown_body_lines(text): start = match.end() if start < len(source): lines.append(source[start:]) - if not lines or lines[0].strip() != "---": + if not lines or not _has_column_zero_frontmatter_opener(text): return lines region_bytes = 0 for index, line in enumerate(lines): @@ -699,18 +709,96 @@ def _without_inline_code_spans(line, open_length=0): return "".join(masked), 0 +def _is_safe_single_line_js_string(value): + """Validate the bounded JavaScript string forms accepted as inert ESM.""" + if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]: + return False + content = value[1:-1] + index = 0 + while index < len(content): + char = content[index] + if char in "\r\n\u2028\u2029": + return False + if char != "\\": + index += 1 + continue + if index + 1 >= len(content): + return False + escaped = content[index + 1] + if escaped in "123456789": + return False + if escaped == "0": + if index + 2 < len(content) and content[index + 2].isdigit(): + return False + index += 2 + continue + if escaped == "x": + digits = content[index + 2 : index + 4] + if len(digits) != 2 or any(char not in string.hexdigits for char in digits): + return False + index += 4 + continue + if escaped == "u": + if index + 2 < len(content) and content[index + 2] == "{": + end = content.find("}", index + 3) + digits = content[index + 3 : end] if end >= 0 else "" + if ( + not 1 <= len(digits) <= 6 + or any(char not in string.hexdigits for char in digits) + or int(digits, 16) > 0x10FFFF + ): + return False + index = end + 1 + continue + digits = content[index + 2 : index + 6] + if len(digits) != 4 or any(char not in string.hexdigits for char in digits): + return False + index += 6 + continue + index += 2 + return True + + def _is_single_line_mdx_esm(line): stripped = line.strip() simple_import = re.fullmatch( - r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])+\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])+')\s*;?", stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" - r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", + r"export\s+const\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) - return simple_import is not None or simple_export is not None + binding = None + value = None + if simple_import is not None: + binding = simple_import.group("import_binding") + value = simple_import.group("import_value") + elif simple_export is not None: + binding = simple_export.group("export_binding") + value = simple_export.group("export_value") + else: + return False + return ( + (binding is None or binding not in _JS_RESERVED_BINDINGS) + and _is_safe_single_line_js_string(value) + ) + + +_JS_RESERVED_BINDINGS = frozenset( + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", + "false", "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", "protected", + "public", "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield", + } +) _RAW_HTML_BLANK_TAGS = frozenset( @@ -776,6 +864,7 @@ def observe_entry_orientation(root, entry): "provider_rendered_title": evidence_value("unavailable"), } body_lines = _markdown_body_lines(text) + frontmatter_opened = _has_column_zero_frontmatter_opener(text) literal_h1 = None if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" @@ -788,11 +877,23 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False + simple_mdx_esm_pending = False in_mdx_expression = False uncertain = False literal_h1 = False for raw_line in body_lines: line = raw_line.rstrip("\r\n") + if simple_mdx_esm_pending: + if _is_ascii_blank_line(line): + simple_mdx_esm_pending = False + continue + if re.match( + r"^(?:import|export)(?:\s|\{|$)", line + ) and _is_single_line_mdx_esm(line): + continue + uncertain = True + in_mdx_esm = True + continue if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -901,8 +1002,11 @@ def observe_entry_orientation(root, entry): if component_document and re.match( r"^(?:import|export)(?:\s|\{|$)", pre_stripped ): - uncertain = True - in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + if _is_single_line_mdx_esm(pre_stripped): + simple_mdx_esm_pending = True + else: + uncertain = True + in_mdx_esm = True continue jsx_match = re.match( r"^|$)", pre_stripped @@ -1071,7 +1175,11 @@ def observe_entry_orientation(root, entry): if body_lines is None: frontmatter_title = evidence_value("unavailable") else: - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + metadata = ( + parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + if frontmatter_opened + else {"status": "absent", "values": {}, "unresolved": []} + ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) if isinstance(title, str) and "title" not in unresolved_metadata: diff --git a/adapters/grok/scripts/_docs_checker/formats.py b/adapters/grok/scripts/_docs_checker/formats.py index d83fff6..ea51f3a 100644 --- a/adapters/grok/scripts/_docs_checker/formats.py +++ b/adapters/grok/scripts/_docs_checker/formats.py @@ -12,6 +12,14 @@ _FRONTMATTER_KEY = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$") +def _is_column_zero_frontmatter_delimiter(line, allowed): + delimiter = line.rstrip("\r\n") + return ( + not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in allowed + ) + + def is_document_path(value): """Return whether a path is supported as inert documentation text.""" return Path(value).suffix.casefold() in DOCUMENT_SUFFIXES @@ -41,7 +49,7 @@ def parse_frontmatter_scalars(text): if not isinstance(text, str): return {"status": "unresolved", "values": {}, "unresolved": ["document"]} lines = text.removeprefix("\ufeff").splitlines(keepends=True) - if not lines or lines[0].strip() != "---": + if not lines or not _is_column_zero_frontmatter_delimiter(lines[0], {"---"}): return {"status": "absent", "values": {}, "unresolved": []} region_bytes = 0 closing = None @@ -49,7 +57,7 @@ def parse_frontmatter_scalars(text): region_bytes += len(line.encode("utf-8", "strict")) if region_bytes > MAX_FRONTMATTER_BYTES: return {"status": "unresolved", "values": {}, "unresolved": ["size"]} - if index and line.strip() in {"---", "..."}: + if index and _is_column_zero_frontmatter_delimiter(line, {"---", "..."}): closing = index break if closing is None: diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 420a95c..551e1cd 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -10,6 +10,7 @@ import json import math import re +import string from collections.abc import Mapping, Sequence from pathlib import Path from urllib.parse import unquote @@ -543,6 +544,15 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _has_column_zero_frontmatter_opener(text): + source = text.removeprefix("\ufeff") + first_line = re.split(r"\r\n|\r|\n", source, maxsplit=1)[0] + return ( + not first_line.startswith((" ", "\t")) + and first_line.rstrip(" \t") == "---" + ) + + def _markdown_body_lines(text): """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" source = text.removeprefix("\ufeff") @@ -553,7 +563,7 @@ def _markdown_body_lines(text): start = match.end() if start < len(source): lines.append(source[start:]) - if not lines or lines[0].strip() != "---": + if not lines or not _has_column_zero_frontmatter_opener(text): return lines region_bytes = 0 for index, line in enumerate(lines): @@ -699,18 +709,96 @@ def _without_inline_code_spans(line, open_length=0): return "".join(masked), 0 +def _is_safe_single_line_js_string(value): + """Validate the bounded JavaScript string forms accepted as inert ESM.""" + if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]: + return False + content = value[1:-1] + index = 0 + while index < len(content): + char = content[index] + if char in "\r\n\u2028\u2029": + return False + if char != "\\": + index += 1 + continue + if index + 1 >= len(content): + return False + escaped = content[index + 1] + if escaped in "123456789": + return False + if escaped == "0": + if index + 2 < len(content) and content[index + 2].isdigit(): + return False + index += 2 + continue + if escaped == "x": + digits = content[index + 2 : index + 4] + if len(digits) != 2 or any(char not in string.hexdigits for char in digits): + return False + index += 4 + continue + if escaped == "u": + if index + 2 < len(content) and content[index + 2] == "{": + end = content.find("}", index + 3) + digits = content[index + 3 : end] if end >= 0 else "" + if ( + not 1 <= len(digits) <= 6 + or any(char not in string.hexdigits for char in digits) + or int(digits, 16) > 0x10FFFF + ): + return False + index = end + 1 + continue + digits = content[index + 2 : index + 6] + if len(digits) != 4 or any(char not in string.hexdigits for char in digits): + return False + index += 6 + continue + index += 2 + return True + + def _is_single_line_mdx_esm(line): stripped = line.strip() simple_import = re.fullmatch( - r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])+\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])+')\s*;?", stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" - r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", + r"export\s+const\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) - return simple_import is not None or simple_export is not None + binding = None + value = None + if simple_import is not None: + binding = simple_import.group("import_binding") + value = simple_import.group("import_value") + elif simple_export is not None: + binding = simple_export.group("export_binding") + value = simple_export.group("export_value") + else: + return False + return ( + (binding is None or binding not in _JS_RESERVED_BINDINGS) + and _is_safe_single_line_js_string(value) + ) + + +_JS_RESERVED_BINDINGS = frozenset( + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", + "false", "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", "protected", + "public", "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield", + } +) _RAW_HTML_BLANK_TAGS = frozenset( @@ -776,6 +864,7 @@ def observe_entry_orientation(root, entry): "provider_rendered_title": evidence_value("unavailable"), } body_lines = _markdown_body_lines(text) + frontmatter_opened = _has_column_zero_frontmatter_opener(text) literal_h1 = None if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" @@ -788,11 +877,23 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False + simple_mdx_esm_pending = False in_mdx_expression = False uncertain = False literal_h1 = False for raw_line in body_lines: line = raw_line.rstrip("\r\n") + if simple_mdx_esm_pending: + if _is_ascii_blank_line(line): + simple_mdx_esm_pending = False + continue + if re.match( + r"^(?:import|export)(?:\s|\{|$)", line + ) and _is_single_line_mdx_esm(line): + continue + uncertain = True + in_mdx_esm = True + continue if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -901,8 +1002,11 @@ def observe_entry_orientation(root, entry): if component_document and re.match( r"^(?:import|export)(?:\s|\{|$)", pre_stripped ): - uncertain = True - in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + if _is_single_line_mdx_esm(pre_stripped): + simple_mdx_esm_pending = True + else: + uncertain = True + in_mdx_esm = True continue jsx_match = re.match( r"^|$)", pre_stripped @@ -1071,7 +1175,11 @@ def observe_entry_orientation(root, entry): if body_lines is None: frontmatter_title = evidence_value("unavailable") else: - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + metadata = ( + parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + if frontmatter_opened + else {"status": "absent", "values": {}, "unresolved": []} + ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) if isinstance(title, str) and "title" not in unresolved_metadata: diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py b/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py index d83fff6..ea51f3a 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/formats.py @@ -12,6 +12,14 @@ _FRONTMATTER_KEY = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$") +def _is_column_zero_frontmatter_delimiter(line, allowed): + delimiter = line.rstrip("\r\n") + return ( + not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in allowed + ) + + def is_document_path(value): """Return whether a path is supported as inert documentation text.""" return Path(value).suffix.casefold() in DOCUMENT_SUFFIXES @@ -41,7 +49,7 @@ def parse_frontmatter_scalars(text): if not isinstance(text, str): return {"status": "unresolved", "values": {}, "unresolved": ["document"]} lines = text.removeprefix("\ufeff").splitlines(keepends=True) - if not lines or lines[0].strip() != "---": + if not lines or not _is_column_zero_frontmatter_delimiter(lines[0], {"---"}): return {"status": "absent", "values": {}, "unresolved": []} region_bytes = 0 closing = None @@ -49,7 +57,7 @@ def parse_frontmatter_scalars(text): region_bytes += len(line.encode("utf-8", "strict")) if region_bytes > MAX_FRONTMATTER_BYTES: return {"status": "unresolved", "values": {}, "unresolved": ["size"]} - if index and line.strip() in {"---", "..."}: + if index and _is_column_zero_frontmatter_delimiter(line, {"---", "..."}): closing = index break if closing is None: diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 420a95c..551e1cd 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -10,6 +10,7 @@ import json import math import re +import string from collections.abc import Mapping, Sequence from pathlib import Path from urllib.parse import unquote @@ -543,6 +544,15 @@ def finding_receipt(kind, *, path=None, line=None, target=None, fingerprint=None } +def _has_column_zero_frontmatter_opener(text): + source = text.removeprefix("\ufeff") + first_line = re.split(r"\r\n|\r|\n", source, maxsplit=1)[0] + return ( + not first_line.startswith((" ", "\t")) + and first_line.rstrip(" \t") == "---" + ) + + def _markdown_body_lines(text): """Return body lines after bounded frontmatter, or None when its boundary is unresolved.""" source = text.removeprefix("\ufeff") @@ -553,7 +563,7 @@ def _markdown_body_lines(text): start = match.end() if start < len(source): lines.append(source[start:]) - if not lines or lines[0].strip() != "---": + if not lines or not _has_column_zero_frontmatter_opener(text): return lines region_bytes = 0 for index, line in enumerate(lines): @@ -699,18 +709,96 @@ def _without_inline_code_spans(line, open_length=0): return "".join(masked), 0 +def _is_safe_single_line_js_string(value): + """Validate the bounded JavaScript string forms accepted as inert ESM.""" + if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]: + return False + content = value[1:-1] + index = 0 + while index < len(content): + char = content[index] + if char in "\r\n\u2028\u2029": + return False + if char != "\\": + index += 1 + continue + if index + 1 >= len(content): + return False + escaped = content[index + 1] + if escaped in "123456789": + return False + if escaped == "0": + if index + 2 < len(content) and content[index + 2].isdigit(): + return False + index += 2 + continue + if escaped == "x": + digits = content[index + 2 : index + 4] + if len(digits) != 2 or any(char not in string.hexdigits for char in digits): + return False + index += 4 + continue + if escaped == "u": + if index + 2 < len(content) and content[index + 2] == "{": + end = content.find("}", index + 3) + digits = content[index + 3 : end] if end >= 0 else "" + if ( + not 1 <= len(digits) <= 6 + or any(char not in string.hexdigits for char in digits) + or int(digits, 16) > 0x10FFFF + ): + return False + index = end + 1 + continue + digits = content[index + 2 : index + 6] + if len(digits) != 4 or any(char not in string.hexdigits for char in digits): + return False + index += 6 + continue + index += 2 + return True + + def _is_single_line_mdx_esm(line): stripped = line.strip() simple_import = re.fullmatch( - r"import(?:\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from)?\s+(['\"])[^'\"\r\n]+\1\s*;?", + r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])+\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])+')\s*;?", stripped, ) simple_export = re.fullmatch( - r"export\s+const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*" - r"(?:\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", + r"export\s+const\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*" + r"(?P\"(?:\\[^\r\n]|[^\"\\\r\n])*\"|" + r"'(?:\\[^\r\n]|[^'\\\r\n])*')\s*;?", stripped, ) - return simple_import is not None or simple_export is not None + binding = None + value = None + if simple_import is not None: + binding = simple_import.group("import_binding") + value = simple_import.group("import_value") + elif simple_export is not None: + binding = simple_export.group("export_binding") + value = simple_export.group("export_value") + else: + return False + return ( + (binding is None or binding not in _JS_RESERVED_BINDINGS) + and _is_safe_single_line_js_string(value) + ) + + +_JS_RESERVED_BINDINGS = frozenset( + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", + "false", "finally", "for", "function", "if", "implements", "import", "in", + "instanceof", "interface", "let", "new", "null", "package", "private", "protected", + "public", "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "var", "void", "while", "with", "yield", + } +) _RAW_HTML_BLANK_TAGS = frozenset( @@ -776,6 +864,7 @@ def observe_entry_orientation(root, entry): "provider_rendered_title": evidence_value("unavailable"), } body_lines = _markdown_body_lines(text) + frontmatter_opened = _has_column_zero_frontmatter_opener(text) literal_h1 = None if body_lines is not None: component_document = Path(relative).suffix.casefold() == ".mdx" @@ -788,11 +877,23 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False + simple_mdx_esm_pending = False in_mdx_expression = False uncertain = False literal_h1 = False for raw_line in body_lines: line = raw_line.rstrip("\r\n") + if simple_mdx_esm_pending: + if _is_ascii_blank_line(line): + simple_mdx_esm_pending = False + continue + if re.match( + r"^(?:import|export)(?:\s|\{|$)", line + ) and _is_single_line_mdx_esm(line): + continue + uncertain = True + in_mdx_esm = True + continue if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -901,8 +1002,11 @@ def observe_entry_orientation(root, entry): if component_document and re.match( r"^(?:import|export)(?:\s|\{|$)", pre_stripped ): - uncertain = True - in_mdx_esm = not _is_single_line_mdx_esm(pre_stripped) + if _is_single_line_mdx_esm(pre_stripped): + simple_mdx_esm_pending = True + else: + uncertain = True + in_mdx_esm = True continue jsx_match = re.match( r"^|$)", pre_stripped @@ -1071,7 +1175,11 @@ def observe_entry_orientation(root, entry): if body_lines is None: frontmatter_title = evidence_value("unavailable") else: - metadata = parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + metadata = ( + parse_frontmatter_scalars(text[: MAX_FRONTMATTER_BYTES + 1]) + if frontmatter_opened + else {"status": "absent", "values": {}, "unresolved": []} + ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) if isinstance(title, str) and "title" not in unresolved_metadata: diff --git a/skills/docs/scripts/_docs_checker/formats.py b/skills/docs/scripts/_docs_checker/formats.py index d83fff6..ea51f3a 100644 --- a/skills/docs/scripts/_docs_checker/formats.py +++ b/skills/docs/scripts/_docs_checker/formats.py @@ -12,6 +12,14 @@ _FRONTMATTER_KEY = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$") +def _is_column_zero_frontmatter_delimiter(line, allowed): + delimiter = line.rstrip("\r\n") + return ( + not delimiter.startswith((" ", "\t")) + and delimiter.rstrip(" \t") in allowed + ) + + def is_document_path(value): """Return whether a path is supported as inert documentation text.""" return Path(value).suffix.casefold() in DOCUMENT_SUFFIXES @@ -41,7 +49,7 @@ def parse_frontmatter_scalars(text): if not isinstance(text, str): return {"status": "unresolved", "values": {}, "unresolved": ["document"]} lines = text.removeprefix("\ufeff").splitlines(keepends=True) - if not lines or lines[0].strip() != "---": + if not lines or not _is_column_zero_frontmatter_delimiter(lines[0], {"---"}): return {"status": "absent", "values": {}, "unresolved": []} region_bytes = 0 closing = None @@ -49,7 +57,7 @@ def parse_frontmatter_scalars(text): region_bytes += len(line.encode("utf-8", "strict")) if region_bytes > MAX_FRONTMATTER_BYTES: return {"status": "unresolved", "values": {}, "unresolved": ["size"]} - if index and line.strip() in {"---", "..."}: + if index and _is_column_zero_frontmatter_delimiter(line, {"---", "..."}): closing = index break if closing is None: diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 10e5f89..8f62e7f 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -480,6 +480,33 @@ def test_orientation_skips_heading_shaped_frontmatter(self): observed["frontmatter_title"], {"status": "unavailable", "value": None} ) + indented_opener = root / "indented-opener.md" + indented_opener.write_text( + " ---\ntitle: Not frontmatter\n# Actual H1\n", encoding="utf-8" + ) + observed = evidence.observe_entry_orientation(root, indented_opener.name) + self.assertEqual( + observed["literal_h1"], {"status": "completed", "value": True} + ) + self.assertEqual( + observed["frontmatter_title"], + {"status": "completed", "value": False}, + ) + + indented_false_close = root / "indented-false-close.md" + indented_false_close.write_text( + "---\ntitle: Good\ndescription: |\n ---\ntitle: Duplicate\n---\n# Actual H1\n", + encoding="utf-8", + ) + observed = evidence.observe_entry_orientation(root, indented_false_close.name) + self.assertEqual( + observed["literal_h1"], {"status": "completed", "value": True} + ) + self.assertEqual( + observed["frontmatter_title"], + {"status": "unavailable", "value": None}, + ) + def test_orientation_ignores_comments_and_indented_code(self): scenarios = { "html-comment": "\n## Start\n", @@ -668,6 +695,54 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): "import Example from './example'\n\n# Actual H1\n", {"status": "completed", "value": True}, ), + "esm-import-no-h1.mdx": ( + "import Example from './example'\n\n## Start\n", + {"status": "completed", "value": False}, + ), + "esm-export-no-h1.mdx": ( + 'export const marker = "safe"\n\n## Start\n', + {"status": "completed", "value": False}, + ), + "esm-import-no-boundary.mdx": ( + "import Example from './example'\n# Not body Markdown\n", + {"status": "unavailable", "value": None}, + ), + "esm-consecutive-imports.mdx": ( + "import One from './one'\nimport Two from './two'\n\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "esm-reserved-import-binding.mdx": ( + "import for from './components'\n\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "esm-reserved-export-binding.mdx": ( + 'export const for = "safe"\n\n# Actual H1\n', + {"status": "unavailable", "value": None}, + ), + "esm-invalid-import-string.mdx": ( + "import Example from '.\\'\n\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "esm-invalid-import-escape.mdx": ( + "import Example from './\\xZZ'\n\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "esm-invalid-side-effect-escape.mdx": ( + "import './\\xZZ'\n\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "esm-invalid-strict-string.mdx": ( + 'export const marker = "\\8"\n\n# Actual H1\n', + {"status": "unavailable", "value": None}, + ), + "esm-invalid-export-hex.mdx": ( + 'export const marker = "\\xZZ"\n\n# Actual H1\n', + {"status": "unavailable", "value": None}, + ), + "esm-invalid-export-unicode.mdx": ( + 'export const marker = "\\u{ZZ}"\n\n# Actual H1\n', + {"status": "unavailable", "value": None}, + ), "esm-named-import-parked.mdx": ( "import { Callout } from './components'\n\n# Actual H1\n", {"status": "unavailable", "value": None}, From b5185eda6610144c05f3b116d4a8adb0a13e1071 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 07:13:14 -0400 Subject: [PATCH 12/20] Recognize immediate MDX headings after ESM --- .../docs/scripts/_docs_checker/evidence.py | 12 +++++++++--- .../copilot/scripts/_docs_checker/evidence.py | 12 +++++++++--- .../cursor/scripts/_docs_checker/evidence.py | 12 +++++++++--- .../grok/scripts/_docs_checker/evidence.py | 12 +++++++++--- .../docs/scripts/_docs_checker/evidence.py | 12 +++++++++--- skills/docs/scripts/_docs_checker/evidence.py | 12 +++++++++--- tests/test_docs_evidence.py | 18 +++++++++++++++--- 7 files changed, 69 insertions(+), 21 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 551e1cd..c695107 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -891,9 +891,15 @@ def observe_entry_orientation(root, entry): r"^(?:import|export)(?:\s|\{|$)", line ) and _is_single_line_mdx_esm(line): continue - uncertain = True - in_mdx_esm = True - continue + heading = line.lstrip(" \t") + if not ( + _leading_indent_columns(line) < 4 + and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + ): + uncertain = True + in_mdx_esm = True + continue + simple_mdx_esm_pending = False if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 551e1cd..c695107 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -891,9 +891,15 @@ def observe_entry_orientation(root, entry): r"^(?:import|export)(?:\s|\{|$)", line ) and _is_single_line_mdx_esm(line): continue - uncertain = True - in_mdx_esm = True - continue + heading = line.lstrip(" \t") + if not ( + _leading_indent_columns(line) < 4 + and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + ): + uncertain = True + in_mdx_esm = True + continue + simple_mdx_esm_pending = False if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 551e1cd..c695107 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -891,9 +891,15 @@ def observe_entry_orientation(root, entry): r"^(?:import|export)(?:\s|\{|$)", line ) and _is_single_line_mdx_esm(line): continue - uncertain = True - in_mdx_esm = True - continue + heading = line.lstrip(" \t") + if not ( + _leading_indent_columns(line) < 4 + and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + ): + uncertain = True + in_mdx_esm = True + continue + simple_mdx_esm_pending = False if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 551e1cd..c695107 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -891,9 +891,15 @@ def observe_entry_orientation(root, entry): r"^(?:import|export)(?:\s|\{|$)", line ) and _is_single_line_mdx_esm(line): continue - uncertain = True - in_mdx_esm = True - continue + heading = line.lstrip(" \t") + if not ( + _leading_indent_columns(line) < 4 + and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + ): + uncertain = True + in_mdx_esm = True + continue + simple_mdx_esm_pending = False if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 551e1cd..c695107 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -891,9 +891,15 @@ def observe_entry_orientation(root, entry): r"^(?:import|export)(?:\s|\{|$)", line ) and _is_single_line_mdx_esm(line): continue - uncertain = True - in_mdx_esm = True - continue + heading = line.lstrip(" \t") + if not ( + _leading_indent_columns(line) < 4 + and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + ): + uncertain = True + in_mdx_esm = True + continue + simple_mdx_esm_pending = False if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 551e1cd..c695107 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -891,9 +891,15 @@ def observe_entry_orientation(root, entry): r"^(?:import|export)(?:\s|\{|$)", line ) and _is_single_line_mdx_esm(line): continue - uncertain = True - in_mdx_esm = True - continue + heading = line.lstrip(" \t") + if not ( + _leading_indent_columns(line) < 4 + and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + ): + uncertain = True + in_mdx_esm = True + continue + simple_mdx_esm_pending = False if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 8f62e7f..97e37ad 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -703,14 +703,26 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): 'export const marker = "safe"\n\n## Start\n', {"status": "completed", "value": False}, ), - "esm-import-no-boundary.mdx": ( - "import Example from './example'\n# Not body Markdown\n", - {"status": "unavailable", "value": None}, + "esm-import-immediate-h1.mdx": ( + "import Example from './example'\n# Actual H1\n", + {"status": "completed", "value": True}, ), "esm-consecutive-imports.mdx": ( "import One from './one'\nimport Two from './two'\n\n# Actual H1\n", {"status": "completed", "value": True}, ), + "esm-consecutive-imports-immediate-h1.mdx": ( + "import One from './one'\nimport Two from './two'\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "esm-export-immediate-no-h1.mdx": ( + 'export const marker = "safe"\n## Start\n', + {"status": "completed", "value": False}, + ), + "esm-import-ambiguous-continuation.mdx": ( + "import Example from './example'\nwith { type: 'json' }\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), "esm-reserved-import-binding.mdx": ( "import for from './components'\n\n# Actual H1\n", {"status": "unavailable", "value": None}, From 3a55d76a95da6fbe91518af0237821ca01b5aebc Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 07:51:14 -0400 Subject: [PATCH 13/20] Harden MDX ESM boundary evidence --- .../docs/scripts/_docs_checker/evidence.py | 77 +++++++++++---- .../copilot/scripts/_docs_checker/evidence.py | 77 +++++++++++---- .../cursor/scripts/_docs_checker/evidence.py | 77 +++++++++++---- .../grok/scripts/_docs_checker/evidence.py | 77 +++++++++++---- .../docs/scripts/_docs_checker/evidence.py | 77 +++++++++++---- skills/docs/scripts/_docs_checker/evidence.py | 77 +++++++++++---- tests/test_docs_evidence.py | 96 +++++++++++++++++++ 7 files changed, 438 insertions(+), 120 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index c695107..423bba9 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -759,7 +759,7 @@ def _is_safe_single_line_js_string(value): return True -def _is_single_line_mdx_esm(line): +def _simple_mdx_esm_info(line): stripped = line.strip() simple_import = re.fullmatch( r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" @@ -782,13 +782,25 @@ def _is_single_line_mdx_esm(line): binding = simple_export.group("export_binding") value = simple_export.group("export_value") else: - return False + return None + if ( + binding is not None + and binding in _JS_RESERVED_BINDINGS + or not _is_safe_single_line_js_string(value) + ): + return None return ( - (binding is None or binding not in _JS_RESERVED_BINDINGS) - and _is_safe_single_line_js_string(value) + "import" if simple_import is not None else "export", + stripped.endswith(";"), ) +def _starts_mdx_esm(line): + return re.match( + r"^(?:import|export)(?![A-Za-z0-9_$])", line + ) is not None + + _JS_RESERVED_BINDINGS = frozenset( { "arguments", "await", "break", "case", "catch", "class", "const", "continue", @@ -841,6 +853,27 @@ def _is_ascii_blank_line(line): return not line.rstrip("\r\n").strip(" \t") +def _is_definite_markdown_after_simple_esm(line, esm_info): + markdown_stripped = line.lstrip(" \t") + if re.match(r"^#{1,6}(?:[ \t]+|$)", markdown_stripped): + return True + kind, explicitly_terminated = esm_info + if explicitly_terminated: + return True + js_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if kind == "import": + if js_stripped.startswith(("//", "/*")): + return False + return re.match( + r"^(?:assert|with)(?![A-Za-z0-9_$])", js_stripped + ) is None + return re.match( + r"^(?:[!+\-*/%<>=&|^?.\[(`,]|in(?![A-Za-z0-9_$])|" + r"instanceof(?![A-Za-z0-9_$]))", + js_stripped, + ) is None + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -877,7 +910,7 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None in_mdx_expression = False uncertain = False literal_h1 = False @@ -885,21 +918,26 @@ def observe_entry_orientation(root, entry): line = raw_line.rstrip("\r\n") if simple_mdx_esm_pending: if _is_ascii_blank_line(line): - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None continue - if re.match( - r"^(?:import|export)(?:\s|\{|$)", line - ) and _is_single_line_mdx_esm(line): + next_esm = None + js_line = re.sub(r"^[\s\ufeff]+", "", line) + if _starts_mdx_esm(js_line): + next_esm = _simple_mdx_esm_info(js_line) + if next_esm is None: + uncertain = True + in_mdx_esm = True + continue + if next_esm is not None: + simple_mdx_esm_pending = next_esm continue - heading = line.lstrip(" \t") - if not ( - _leading_indent_columns(line) < 4 - and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + if not _is_definite_markdown_after_simple_esm( + line, simple_mdx_esm_pending ): uncertain = True in_mdx_esm = True continue - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -993,7 +1031,7 @@ def observe_entry_orientation(root, entry): component_document and ( interrupt.startswith("{") - or re.match(r"^(?:import|export)(?:\s|\{|$)", interrupt) + or _starts_mdx_esm(interrupt) ) ) ): @@ -1005,11 +1043,10 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and re.match( - r"^(?:import|export)(?:\s|\{|$)", pre_stripped - ): - if _is_single_line_mdx_esm(pre_stripped): - simple_mdx_esm_pending = True + if component_document and _starts_mdx_esm(pre_stripped): + esm_info = _simple_mdx_esm_info(pre_stripped) + if esm_info is not None: + simple_mdx_esm_pending = esm_info else: uncertain = True in_mdx_esm = True diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index c695107..423bba9 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -759,7 +759,7 @@ def _is_safe_single_line_js_string(value): return True -def _is_single_line_mdx_esm(line): +def _simple_mdx_esm_info(line): stripped = line.strip() simple_import = re.fullmatch( r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" @@ -782,13 +782,25 @@ def _is_single_line_mdx_esm(line): binding = simple_export.group("export_binding") value = simple_export.group("export_value") else: - return False + return None + if ( + binding is not None + and binding in _JS_RESERVED_BINDINGS + or not _is_safe_single_line_js_string(value) + ): + return None return ( - (binding is None or binding not in _JS_RESERVED_BINDINGS) - and _is_safe_single_line_js_string(value) + "import" if simple_import is not None else "export", + stripped.endswith(";"), ) +def _starts_mdx_esm(line): + return re.match( + r"^(?:import|export)(?![A-Za-z0-9_$])", line + ) is not None + + _JS_RESERVED_BINDINGS = frozenset( { "arguments", "await", "break", "case", "catch", "class", "const", "continue", @@ -841,6 +853,27 @@ def _is_ascii_blank_line(line): return not line.rstrip("\r\n").strip(" \t") +def _is_definite_markdown_after_simple_esm(line, esm_info): + markdown_stripped = line.lstrip(" \t") + if re.match(r"^#{1,6}(?:[ \t]+|$)", markdown_stripped): + return True + kind, explicitly_terminated = esm_info + if explicitly_terminated: + return True + js_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if kind == "import": + if js_stripped.startswith(("//", "/*")): + return False + return re.match( + r"^(?:assert|with)(?![A-Za-z0-9_$])", js_stripped + ) is None + return re.match( + r"^(?:[!+\-*/%<>=&|^?.\[(`,]|in(?![A-Za-z0-9_$])|" + r"instanceof(?![A-Za-z0-9_$]))", + js_stripped, + ) is None + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -877,7 +910,7 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None in_mdx_expression = False uncertain = False literal_h1 = False @@ -885,21 +918,26 @@ def observe_entry_orientation(root, entry): line = raw_line.rstrip("\r\n") if simple_mdx_esm_pending: if _is_ascii_blank_line(line): - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None continue - if re.match( - r"^(?:import|export)(?:\s|\{|$)", line - ) and _is_single_line_mdx_esm(line): + next_esm = None + js_line = re.sub(r"^[\s\ufeff]+", "", line) + if _starts_mdx_esm(js_line): + next_esm = _simple_mdx_esm_info(js_line) + if next_esm is None: + uncertain = True + in_mdx_esm = True + continue + if next_esm is not None: + simple_mdx_esm_pending = next_esm continue - heading = line.lstrip(" \t") - if not ( - _leading_indent_columns(line) < 4 - and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + if not _is_definite_markdown_after_simple_esm( + line, simple_mdx_esm_pending ): uncertain = True in_mdx_esm = True continue - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -993,7 +1031,7 @@ def observe_entry_orientation(root, entry): component_document and ( interrupt.startswith("{") - or re.match(r"^(?:import|export)(?:\s|\{|$)", interrupt) + or _starts_mdx_esm(interrupt) ) ) ): @@ -1005,11 +1043,10 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and re.match( - r"^(?:import|export)(?:\s|\{|$)", pre_stripped - ): - if _is_single_line_mdx_esm(pre_stripped): - simple_mdx_esm_pending = True + if component_document and _starts_mdx_esm(pre_stripped): + esm_info = _simple_mdx_esm_info(pre_stripped) + if esm_info is not None: + simple_mdx_esm_pending = esm_info else: uncertain = True in_mdx_esm = True diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index c695107..423bba9 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -759,7 +759,7 @@ def _is_safe_single_line_js_string(value): return True -def _is_single_line_mdx_esm(line): +def _simple_mdx_esm_info(line): stripped = line.strip() simple_import = re.fullmatch( r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" @@ -782,13 +782,25 @@ def _is_single_line_mdx_esm(line): binding = simple_export.group("export_binding") value = simple_export.group("export_value") else: - return False + return None + if ( + binding is not None + and binding in _JS_RESERVED_BINDINGS + or not _is_safe_single_line_js_string(value) + ): + return None return ( - (binding is None or binding not in _JS_RESERVED_BINDINGS) - and _is_safe_single_line_js_string(value) + "import" if simple_import is not None else "export", + stripped.endswith(";"), ) +def _starts_mdx_esm(line): + return re.match( + r"^(?:import|export)(?![A-Za-z0-9_$])", line + ) is not None + + _JS_RESERVED_BINDINGS = frozenset( { "arguments", "await", "break", "case", "catch", "class", "const", "continue", @@ -841,6 +853,27 @@ def _is_ascii_blank_line(line): return not line.rstrip("\r\n").strip(" \t") +def _is_definite_markdown_after_simple_esm(line, esm_info): + markdown_stripped = line.lstrip(" \t") + if re.match(r"^#{1,6}(?:[ \t]+|$)", markdown_stripped): + return True + kind, explicitly_terminated = esm_info + if explicitly_terminated: + return True + js_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if kind == "import": + if js_stripped.startswith(("//", "/*")): + return False + return re.match( + r"^(?:assert|with)(?![A-Za-z0-9_$])", js_stripped + ) is None + return re.match( + r"^(?:[!+\-*/%<>=&|^?.\[(`,]|in(?![A-Za-z0-9_$])|" + r"instanceof(?![A-Za-z0-9_$]))", + js_stripped, + ) is None + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -877,7 +910,7 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None in_mdx_expression = False uncertain = False literal_h1 = False @@ -885,21 +918,26 @@ def observe_entry_orientation(root, entry): line = raw_line.rstrip("\r\n") if simple_mdx_esm_pending: if _is_ascii_blank_line(line): - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None continue - if re.match( - r"^(?:import|export)(?:\s|\{|$)", line - ) and _is_single_line_mdx_esm(line): + next_esm = None + js_line = re.sub(r"^[\s\ufeff]+", "", line) + if _starts_mdx_esm(js_line): + next_esm = _simple_mdx_esm_info(js_line) + if next_esm is None: + uncertain = True + in_mdx_esm = True + continue + if next_esm is not None: + simple_mdx_esm_pending = next_esm continue - heading = line.lstrip(" \t") - if not ( - _leading_indent_columns(line) < 4 - and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + if not _is_definite_markdown_after_simple_esm( + line, simple_mdx_esm_pending ): uncertain = True in_mdx_esm = True continue - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -993,7 +1031,7 @@ def observe_entry_orientation(root, entry): component_document and ( interrupt.startswith("{") - or re.match(r"^(?:import|export)(?:\s|\{|$)", interrupt) + or _starts_mdx_esm(interrupt) ) ) ): @@ -1005,11 +1043,10 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and re.match( - r"^(?:import|export)(?:\s|\{|$)", pre_stripped - ): - if _is_single_line_mdx_esm(pre_stripped): - simple_mdx_esm_pending = True + if component_document and _starts_mdx_esm(pre_stripped): + esm_info = _simple_mdx_esm_info(pre_stripped) + if esm_info is not None: + simple_mdx_esm_pending = esm_info else: uncertain = True in_mdx_esm = True diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index c695107..423bba9 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -759,7 +759,7 @@ def _is_safe_single_line_js_string(value): return True -def _is_single_line_mdx_esm(line): +def _simple_mdx_esm_info(line): stripped = line.strip() simple_import = re.fullmatch( r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" @@ -782,13 +782,25 @@ def _is_single_line_mdx_esm(line): binding = simple_export.group("export_binding") value = simple_export.group("export_value") else: - return False + return None + if ( + binding is not None + and binding in _JS_RESERVED_BINDINGS + or not _is_safe_single_line_js_string(value) + ): + return None return ( - (binding is None or binding not in _JS_RESERVED_BINDINGS) - and _is_safe_single_line_js_string(value) + "import" if simple_import is not None else "export", + stripped.endswith(";"), ) +def _starts_mdx_esm(line): + return re.match( + r"^(?:import|export)(?![A-Za-z0-9_$])", line + ) is not None + + _JS_RESERVED_BINDINGS = frozenset( { "arguments", "await", "break", "case", "catch", "class", "const", "continue", @@ -841,6 +853,27 @@ def _is_ascii_blank_line(line): return not line.rstrip("\r\n").strip(" \t") +def _is_definite_markdown_after_simple_esm(line, esm_info): + markdown_stripped = line.lstrip(" \t") + if re.match(r"^#{1,6}(?:[ \t]+|$)", markdown_stripped): + return True + kind, explicitly_terminated = esm_info + if explicitly_terminated: + return True + js_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if kind == "import": + if js_stripped.startswith(("//", "/*")): + return False + return re.match( + r"^(?:assert|with)(?![A-Za-z0-9_$])", js_stripped + ) is None + return re.match( + r"^(?:[!+\-*/%<>=&|^?.\[(`,]|in(?![A-Za-z0-9_$])|" + r"instanceof(?![A-Za-z0-9_$]))", + js_stripped, + ) is None + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -877,7 +910,7 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None in_mdx_expression = False uncertain = False literal_h1 = False @@ -885,21 +918,26 @@ def observe_entry_orientation(root, entry): line = raw_line.rstrip("\r\n") if simple_mdx_esm_pending: if _is_ascii_blank_line(line): - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None continue - if re.match( - r"^(?:import|export)(?:\s|\{|$)", line - ) and _is_single_line_mdx_esm(line): + next_esm = None + js_line = re.sub(r"^[\s\ufeff]+", "", line) + if _starts_mdx_esm(js_line): + next_esm = _simple_mdx_esm_info(js_line) + if next_esm is None: + uncertain = True + in_mdx_esm = True + continue + if next_esm is not None: + simple_mdx_esm_pending = next_esm continue - heading = line.lstrip(" \t") - if not ( - _leading_indent_columns(line) < 4 - and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + if not _is_definite_markdown_after_simple_esm( + line, simple_mdx_esm_pending ): uncertain = True in_mdx_esm = True continue - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -993,7 +1031,7 @@ def observe_entry_orientation(root, entry): component_document and ( interrupt.startswith("{") - or re.match(r"^(?:import|export)(?:\s|\{|$)", interrupt) + or _starts_mdx_esm(interrupt) ) ) ): @@ -1005,11 +1043,10 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and re.match( - r"^(?:import|export)(?:\s|\{|$)", pre_stripped - ): - if _is_single_line_mdx_esm(pre_stripped): - simple_mdx_esm_pending = True + if component_document and _starts_mdx_esm(pre_stripped): + esm_info = _simple_mdx_esm_info(pre_stripped) + if esm_info is not None: + simple_mdx_esm_pending = esm_info else: uncertain = True in_mdx_esm = True diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index c695107..423bba9 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -759,7 +759,7 @@ def _is_safe_single_line_js_string(value): return True -def _is_single_line_mdx_esm(line): +def _simple_mdx_esm_info(line): stripped = line.strip() simple_import = re.fullmatch( r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" @@ -782,13 +782,25 @@ def _is_single_line_mdx_esm(line): binding = simple_export.group("export_binding") value = simple_export.group("export_value") else: - return False + return None + if ( + binding is not None + and binding in _JS_RESERVED_BINDINGS + or not _is_safe_single_line_js_string(value) + ): + return None return ( - (binding is None or binding not in _JS_RESERVED_BINDINGS) - and _is_safe_single_line_js_string(value) + "import" if simple_import is not None else "export", + stripped.endswith(";"), ) +def _starts_mdx_esm(line): + return re.match( + r"^(?:import|export)(?![A-Za-z0-9_$])", line + ) is not None + + _JS_RESERVED_BINDINGS = frozenset( { "arguments", "await", "break", "case", "catch", "class", "const", "continue", @@ -841,6 +853,27 @@ def _is_ascii_blank_line(line): return not line.rstrip("\r\n").strip(" \t") +def _is_definite_markdown_after_simple_esm(line, esm_info): + markdown_stripped = line.lstrip(" \t") + if re.match(r"^#{1,6}(?:[ \t]+|$)", markdown_stripped): + return True + kind, explicitly_terminated = esm_info + if explicitly_terminated: + return True + js_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if kind == "import": + if js_stripped.startswith(("//", "/*")): + return False + return re.match( + r"^(?:assert|with)(?![A-Za-z0-9_$])", js_stripped + ) is None + return re.match( + r"^(?:[!+\-*/%<>=&|^?.\[(`,]|in(?![A-Za-z0-9_$])|" + r"instanceof(?![A-Za-z0-9_$]))", + js_stripped, + ) is None + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -877,7 +910,7 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None in_mdx_expression = False uncertain = False literal_h1 = False @@ -885,21 +918,26 @@ def observe_entry_orientation(root, entry): line = raw_line.rstrip("\r\n") if simple_mdx_esm_pending: if _is_ascii_blank_line(line): - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None continue - if re.match( - r"^(?:import|export)(?:\s|\{|$)", line - ) and _is_single_line_mdx_esm(line): + next_esm = None + js_line = re.sub(r"^[\s\ufeff]+", "", line) + if _starts_mdx_esm(js_line): + next_esm = _simple_mdx_esm_info(js_line) + if next_esm is None: + uncertain = True + in_mdx_esm = True + continue + if next_esm is not None: + simple_mdx_esm_pending = next_esm continue - heading = line.lstrip(" \t") - if not ( - _leading_indent_columns(line) < 4 - and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + if not _is_definite_markdown_after_simple_esm( + line, simple_mdx_esm_pending ): uncertain = True in_mdx_esm = True continue - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -993,7 +1031,7 @@ def observe_entry_orientation(root, entry): component_document and ( interrupt.startswith("{") - or re.match(r"^(?:import|export)(?:\s|\{|$)", interrupt) + or _starts_mdx_esm(interrupt) ) ) ): @@ -1005,11 +1043,10 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and re.match( - r"^(?:import|export)(?:\s|\{|$)", pre_stripped - ): - if _is_single_line_mdx_esm(pre_stripped): - simple_mdx_esm_pending = True + if component_document and _starts_mdx_esm(pre_stripped): + esm_info = _simple_mdx_esm_info(pre_stripped) + if esm_info is not None: + simple_mdx_esm_pending = esm_info else: uncertain = True in_mdx_esm = True diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index c695107..423bba9 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -759,7 +759,7 @@ def _is_safe_single_line_js_string(value): return True -def _is_single_line_mdx_esm(line): +def _simple_mdx_esm_info(line): stripped = line.strip() simple_import = re.fullmatch( r"import(?:\s+(?P[A-Za-z_$][A-Za-z0-9_$]*)\s+from)?\s+" @@ -782,13 +782,25 @@ def _is_single_line_mdx_esm(line): binding = simple_export.group("export_binding") value = simple_export.group("export_value") else: - return False + return None + if ( + binding is not None + and binding in _JS_RESERVED_BINDINGS + or not _is_safe_single_line_js_string(value) + ): + return None return ( - (binding is None or binding not in _JS_RESERVED_BINDINGS) - and _is_safe_single_line_js_string(value) + "import" if simple_import is not None else "export", + stripped.endswith(";"), ) +def _starts_mdx_esm(line): + return re.match( + r"^(?:import|export)(?![A-Za-z0-9_$])", line + ) is not None + + _JS_RESERVED_BINDINGS = frozenset( { "arguments", "await", "break", "case", "catch", "class", "const", "continue", @@ -841,6 +853,27 @@ def _is_ascii_blank_line(line): return not line.rstrip("\r\n").strip(" \t") +def _is_definite_markdown_after_simple_esm(line, esm_info): + markdown_stripped = line.lstrip(" \t") + if re.match(r"^#{1,6}(?:[ \t]+|$)", markdown_stripped): + return True + kind, explicitly_terminated = esm_info + if explicitly_terminated: + return True + js_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if kind == "import": + if js_stripped.startswith(("//", "/*")): + return False + return re.match( + r"^(?:assert|with)(?![A-Za-z0-9_$])", js_stripped + ) is None + return re.match( + r"^(?:[!+\-*/%<>=&|^?.\[(`,]|in(?![A-Za-z0-9_$])|" + r"instanceof(?![A-Za-z0-9_$]))", + js_stripped, + ) is None + + def observe_entry_orientation(root, entry): """Read bounded inert text evidence; never evaluate provider or MDX code.""" if entry is None: @@ -877,7 +910,7 @@ def observe_entry_orientation(root, entry): raw_html_terminator = None raw_html_until_blank = False in_mdx_esm = False - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None in_mdx_expression = False uncertain = False literal_h1 = False @@ -885,21 +918,26 @@ def observe_entry_orientation(root, entry): line = raw_line.rstrip("\r\n") if simple_mdx_esm_pending: if _is_ascii_blank_line(line): - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None continue - if re.match( - r"^(?:import|export)(?:\s|\{|$)", line - ) and _is_single_line_mdx_esm(line): + next_esm = None + js_line = re.sub(r"^[\s\ufeff]+", "", line) + if _starts_mdx_esm(js_line): + next_esm = _simple_mdx_esm_info(js_line) + if next_esm is None: + uncertain = True + in_mdx_esm = True + continue + if next_esm is not None: + simple_mdx_esm_pending = next_esm continue - heading = line.lstrip(" \t") - if not ( - _leading_indent_columns(line) < 4 - and re.match(r"^#{1,6}(?:[ \t]+|$)", heading) + if not _is_definite_markdown_after_simple_esm( + line, simple_mdx_esm_pending ): uncertain = True in_mdx_esm = True continue - simple_mdx_esm_pending = False + simple_mdx_esm_pending = None if in_html_comment: if not html_comment_block and ( _is_ascii_blank_line(line) @@ -993,7 +1031,7 @@ def observe_entry_orientation(root, entry): component_document and ( interrupt.startswith("{") - or re.match(r"^(?:import|export)(?:\s|\{|$)", interrupt) + or _starts_mdx_esm(interrupt) ) ) ): @@ -1005,11 +1043,10 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and re.match( - r"^(?:import|export)(?:\s|\{|$)", pre_stripped - ): - if _is_single_line_mdx_esm(pre_stripped): - simple_mdx_esm_pending = True + if component_document and _starts_mdx_esm(pre_stripped): + esm_info = _simple_mdx_esm_info(pre_stripped) + if esm_info is not None: + simple_mdx_esm_pending = esm_info else: uncertain = True in_mdx_esm = True diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 97e37ad..3d294fb 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -715,6 +715,34 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): "import One from './one'\nimport Two from './two'\n# Actual H1\n", {"status": "completed", "value": True}, ), + "esm-consecutive-safe-esm-form-feed.mdx": ( + 'export const one = "safe"\n\fexport const two = "safe"\n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "esm-consecutive-unsafe-esm-form-feed.mdx": ( + 'export const one = "safe"\n\fexport const two = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-consecutive-unsafe-esm-nbsp.mdx": ( + 'import One from "./one"\n\u00a0export const two = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-consecutive-unsafe-esm-bom.mdx": ( + 'import One from "./one"\n\ufeffexport const two = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-initial-export-comment-boundary.mdx": ( + 'export/*continued\n# not a heading\n*/ const marker = "safe"\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-pending-export-comment-boundary.mdx": ( + 'import One from "./one"\nexport/*continued\n# not a heading\n*/ const marker = "safe"\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-pending-import-comment-boundary.mdx": ( + 'export const one = "safe"\nimport/*continued\n# not a heading\n*/ Two from "./two"\n## Start\n', + {"status": "unavailable", "value": None}, + ), "esm-export-immediate-no-h1.mdx": ( 'export const marker = "safe"\n## Start\n', {"status": "completed", "value": False}, @@ -723,6 +751,74 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): "import Example from './example'\nwith { type: 'json' }\n# Actual H1\n", {"status": "unavailable", "value": None}, ), + "esm-import-commented-attribute-continuation.mdx": ( + "import Example from './example'\nwith/*continued\n# not a heading\n*/ { type: 'json' }\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "esm-import-pre-keyword-block-comment.mdx": ( + "import Example from './example'\n/*continued\n# not a heading\n*/ with { type: 'json' }\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "esm-import-pre-keyword-line-comment.mdx": ( + "import Example from './example'\n// continued\nwith { type: 'json' }\n# Actual H1\n", + {"status": "unavailable", "value": None}, + ), + "esm-import-nbsp-before-attribute.mdx": ( + "import Example from './example'\n\u00a0with/*continued\n# not a heading\n*/ { type: 'json' }\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "esm-import-bom-before-attribute.mdx": ( + "import Example from './example'\n\ufeffwith/*continued\n# not a heading\n*/ { type: 'json' }\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "esm-import-immediate-paragraph-h1.mdx": ( + "import Example from './example'\nIntro\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "esm-export-immediate-paragraph-h1.mdx": ( + 'export const marker = "safe"\nIntro text.\n# Actual H1\n', + {"status": "completed", "value": True}, + ), + "esm-export-immediate-paragraph-no-h1.mdx": ( + 'export const marker = "safe"\nIntro text.\n## Start\n', + {"status": "completed", "value": False}, + ), + "esm-export-operator-continuation.mdx": ( + 'export const marker = "safe"\nin object\n# Actual H1\n', + {"status": "unavailable", "value": None}, + ), + "esm-export-punctuation-continuation.mdx": ( + 'export const marker = "safe"\n+ `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-export-not-equal-continuation.mdx": ( + 'export const marker = "safe"\n!= `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-export-instanceof-comment-continuation.mdx": ( + 'export const marker = "safe"\ninstanceof/*continued\n# not a heading\n*/ Object\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-export-in-comment-continuation.mdx": ( + 'export const marker = "safe"\nin/*continued\n# not a heading\n*/ Object\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-export-form-feed-in-continuation.mdx": ( + 'export const marker = "safe"\n\fin/*continued\n# not a heading\n*/ Object\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-export-form-feed-plus-continuation.mdx": ( + 'export const marker = "safe"\n\f+ `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-import-immediate-list-h1.mdx": ( + "import Example from './example'\n- Intro\n# Actual H1\n", + {"status": "completed", "value": True}, + ), + "esm-export-unicode-paragraph-h1.mdx": ( + 'export const marker = "safe"\nRésumé.\n# Actual H1\n', + {"status": "completed", "value": True}, + ), "esm-reserved-import-binding.mdx": ( "import for from './components'\n\n# Actual H1\n", {"status": "unavailable", "value": None}, From 1105ecdd52c3d40b7cd460d1298ad908928aa895 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 08:04:36 -0400 Subject: [PATCH 14/20] Require semantic evaluator provenance --- .../docs/scripts/_docs_checker/evidence.py | 7 +++++++ .../copilot/scripts/_docs_checker/evidence.py | 7 +++++++ .../cursor/scripts/_docs_checker/evidence.py | 7 +++++++ adapters/grok/scripts/_docs_checker/evidence.py | 7 +++++++ .../docs/scripts/_docs_checker/evidence.py | 7 +++++++ skills/docs/scripts/_docs_checker/evidence.py | 7 +++++++ tests/test_docs_evidence.py | 17 +++++++++++++++++ 7 files changed, 59 insertions(+) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 423bba9..200d324 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -321,6 +321,13 @@ def _validate_lane(value, name, *, semantic=False): f"{name}.evaluator.{field}", validator=_safe_identifier, ) + if value["status"] == "completed": + for field in ("provider", "model"): + if evaluator[field]["status"] != "completed": + raise ValueError( + f"{name}.evaluator.{field} must be completed " + "when semantic evidence is completed" + ) def _validate_unresolved(value): diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 423bba9..200d324 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -321,6 +321,13 @@ def _validate_lane(value, name, *, semantic=False): f"{name}.evaluator.{field}", validator=_safe_identifier, ) + if value["status"] == "completed": + for field in ("provider", "model"): + if evaluator[field]["status"] != "completed": + raise ValueError( + f"{name}.evaluator.{field} must be completed " + "when semantic evidence is completed" + ) def _validate_unresolved(value): diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 423bba9..200d324 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -321,6 +321,13 @@ def _validate_lane(value, name, *, semantic=False): f"{name}.evaluator.{field}", validator=_safe_identifier, ) + if value["status"] == "completed": + for field in ("provider", "model"): + if evaluator[field]["status"] != "completed": + raise ValueError( + f"{name}.evaluator.{field} must be completed " + "when semantic evidence is completed" + ) def _validate_unresolved(value): diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 423bba9..200d324 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -321,6 +321,13 @@ def _validate_lane(value, name, *, semantic=False): f"{name}.evaluator.{field}", validator=_safe_identifier, ) + if value["status"] == "completed": + for field in ("provider", "model"): + if evaluator[field]["status"] != "completed": + raise ValueError( + f"{name}.evaluator.{field} must be completed " + "when semantic evidence is completed" + ) def _validate_unresolved(value): diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 423bba9..200d324 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -321,6 +321,13 @@ def _validate_lane(value, name, *, semantic=False): f"{name}.evaluator.{field}", validator=_safe_identifier, ) + if value["status"] == "completed": + for field in ("provider", "model"): + if evaluator[field]["status"] != "completed": + raise ValueError( + f"{name}.evaluator.{field} must be completed " + "when semantic evidence is completed" + ) def _validate_unresolved(value): diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 423bba9..200d324 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -321,6 +321,13 @@ def _validate_lane(value, name, *, semantic=False): f"{name}.evaluator.{field}", validator=_safe_identifier, ) + if value["status"] == "completed": + for field in ("provider", "model"): + if evaluator[field]["status"] != "completed": + raise ValueError( + f"{name}.evaluator.{field} must be completed " + "when semantic evidence is completed" + ) def _validate_unresolved(value): diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 3d294fb..67f05f1 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -109,6 +109,23 @@ def test_sanitized_dogfood_receipt_is_complete_and_explicit(self): self.assertEqual(self.receipt["run"]["duration_seconds"]["value"], 508.747) self.assertIn("repository.commit", self.receipt["unavailable_evidence"]) + def test_completed_semantic_lane_requires_evaluator_provenance(self): + for field in ("provider", "model"): + for status in ("not_assessed", "unavailable", "failed"): + value = copy.deepcopy(self.receipt) + value["evidence"]["semantic"]["evaluator"][field] = { + "status": status, + "value": None, + } + unavailable = set(value["unavailable_evidence"]) + unavailable.add(f"evidence.semantic.evaluator.{field}") + value["unavailable_evidence"] = sorted(unavailable) + with self.subTest(field=field, status=status), self.assertRaisesRegex( + ValueError, + rf"evidence\.semantic\.evaluator\.{field} must be completed", + ): + evidence.validate_evidence_receipt(value) + def test_receipt_rejects_unknown_sensitive_and_absolute_data(self): for mutation in ("unknown", "raw_transcript"): value = copy.deepcopy(self.receipt) From 0595db8a74277eb6bb932bdc7da3fa400c7de6d0 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 08:20:13 -0400 Subject: [PATCH 15/20] Fail closed on initial MDX ESM whitespace --- .../docs/scripts/_docs_checker/evidence.py | 5 +++-- .../copilot/scripts/_docs_checker/evidence.py | 5 +++-- .../cursor/scripts/_docs_checker/evidence.py | 5 +++-- adapters/grok/scripts/_docs_checker/evidence.py | 5 +++-- .../docs/scripts/_docs_checker/evidence.py | 5 +++-- skills/docs/scripts/_docs_checker/evidence.py | 5 +++-- tests/test_docs_evidence.py | 16 ++++++++++++++++ 7 files changed, 34 insertions(+), 12 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index 200d324..d51dd66 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -1050,8 +1050,9 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and _starts_mdx_esm(pre_stripped): - esm_info = _simple_mdx_esm_info(pre_stripped) + js_pre_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if component_document and _starts_mdx_esm(js_pre_stripped): + esm_info = _simple_mdx_esm_info(js_pre_stripped) if esm_info is not None: simple_mdx_esm_pending = esm_info else: diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index 200d324..d51dd66 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -1050,8 +1050,9 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and _starts_mdx_esm(pre_stripped): - esm_info = _simple_mdx_esm_info(pre_stripped) + js_pre_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if component_document and _starts_mdx_esm(js_pre_stripped): + esm_info = _simple_mdx_esm_info(js_pre_stripped) if esm_info is not None: simple_mdx_esm_pending = esm_info else: diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index 200d324..d51dd66 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -1050,8 +1050,9 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and _starts_mdx_esm(pre_stripped): - esm_info = _simple_mdx_esm_info(pre_stripped) + js_pre_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if component_document and _starts_mdx_esm(js_pre_stripped): + esm_info = _simple_mdx_esm_info(js_pre_stripped) if esm_info is not None: simple_mdx_esm_pending = esm_info else: diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index 200d324..d51dd66 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -1050,8 +1050,9 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and _starts_mdx_esm(pre_stripped): - esm_info = _simple_mdx_esm_info(pre_stripped) + js_pre_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if component_document and _starts_mdx_esm(js_pre_stripped): + esm_info = _simple_mdx_esm_info(js_pre_stripped) if esm_info is not None: simple_mdx_esm_pending = esm_info else: diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index 200d324..d51dd66 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -1050,8 +1050,9 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and _starts_mdx_esm(pre_stripped): - esm_info = _simple_mdx_esm_info(pre_stripped) + js_pre_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if component_document and _starts_mdx_esm(js_pre_stripped): + esm_info = _simple_mdx_esm_info(js_pre_stripped) if esm_info is not None: simple_mdx_esm_pending = esm_info else: diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index 200d324..d51dd66 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -1050,8 +1050,9 @@ def observe_entry_orientation(root, entry): if inline_code_length == 0 and _leading_indent_columns(line) >= 4: continue pre_stripped = line.lstrip(" \t") - if component_document and _starts_mdx_esm(pre_stripped): - esm_info = _simple_mdx_esm_info(pre_stripped) + js_pre_stripped = re.sub(r"^[\s\ufeff]+", "", line) + if component_document and _starts_mdx_esm(js_pre_stripped): + esm_info = _simple_mdx_esm_info(js_pre_stripped) if esm_info is not None: simple_mdx_esm_pending = esm_info else: diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 67f05f1..5591050 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -748,6 +748,22 @@ def test_orientation_respects_markdown_and_mdx_contexts(self): 'import One from "./one"\n\ufeffexport const two = `continued\n# not a heading\n`\n## Start\n', {"status": "unavailable", "value": None}, ), + "esm-initial-unsafe-esm-nbsp.mdx": ( + '\u00a0export const data = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-initial-unsafe-esm-form-feed.mdx": ( + '\fexport const data = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-initial-unsafe-esm-em-space.mdx": ( + '\u2003export const data = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), + "esm-initial-unsafe-esm-bom.mdx": ( + '\ufeffexport const data = `continued\n# not a heading\n`\n## Start\n', + {"status": "unavailable", "value": None}, + ), "esm-initial-export-comment-boundary.mdx": ( 'export/*continued\n# not a heading\n*/ const marker = "safe"\n## Start\n', {"status": "unavailable", "value": None}, From b72cd86ee8ccea429b35d695532249303b3b2fc4 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 08:36:28 -0400 Subject: [PATCH 16/20] Protect corpus outputs from hard-link aliases --- tests/test_docs_evidence.py | 23 +++++++++++++++++++++++ tools/run_docs_corpus.py | 27 +++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 5591050..4136346 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -1715,6 +1715,29 @@ def test_runner_output_cannot_write_into_workspace_or_through_reparse(self): with self.assertRaisesRegex(ValueError, "symlink|reparse"): run_docs_corpus._output_path(alias / "receipt.json", workspace) + def test_runner_output_replaces_hard_link_without_mutating_checkout(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + workspace = root / "workspace" + checkout = workspace / "cline" + checkout.mkdir(parents=True) + checkout_file = checkout / "tracked.json" + checkout_file.write_text("checkout bytes\n", encoding="utf-8") + output = root / "baseline.json" + os.link(checkout_file, output) + result = {"schema_version": 1, "repositories": []} + with mock.patch.object( + run_docs_corpus, "run_corpus", return_value=result + ): + self.assertEqual( + run_docs_corpus.main( + ["--workspace", str(workspace), "--output", str(output)] + ), + 0, + ) + self.assertEqual(checkout_file.read_text(encoding="utf-8"), "checkout bytes\n") + self.assertEqual(json.loads(output.read_text(encoding="utf-8")), result) + def test_prepare_reparse_workspace_cannot_write_outside(self): with tempfile.TemporaryDirectory() as td: original = prepare_docs_corpus.WORKSPACE_ROOT diff --git a/tools/run_docs_corpus.py b/tools/run_docs_corpus.py index cfe5f03..415036f 100644 --- a/tools/run_docs_corpus.py +++ b/tools/run_docs_corpus.py @@ -9,6 +9,7 @@ import re import subprocess import sys +import tempfile from pathlib import Path @@ -316,6 +317,29 @@ def _output_path(value, workspace): raise ValueError("corpus output must remain outside the corpus workspace") +def _write_output(output, encoded): + """Atomically replace output without following an existing hard link.""" + _assert_no_reparse_components(output) + descriptor, temporary_name = tempfile.mkstemp( + dir=output.parent, + prefix=f".{output.name}.", + suffix=".tmp", + ) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream: + descriptor = None + stream.write(encoded) + _assert_no_reparse_components(output) + os.replace(temporary, output) + temporary = None + finally: + if descriptor is not None: + os.close(descriptor) + if temporary is not None: + temporary.unlink(missing_ok=True) + + def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument("--manifest", default=str(DEFAULT_MANIFEST)) @@ -327,8 +351,7 @@ def main(argv=None): result = run_corpus(namespace.manifest, namespace.workspace) encoded = json.dumps(result, indent=2, sort_keys=True, ensure_ascii=True) + "\n" if output: - _assert_no_reparse_components(output) - output.write_text(encoded, encoding="utf-8", newline="\n") + _write_output(output, encoded) else: print(encoded, end="") return 0 From 7c8c0cd782a5a57c4ee122a46236f304ada75d1f Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 08:43:04 -0400 Subject: [PATCH 17/20] Support near-limit corpus output names --- tests/test_docs_evidence.py | 19 +++++++++++++++++++ tools/run_docs_corpus.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 4136346..d899928 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -1738,6 +1738,25 @@ def test_runner_output_replaces_hard_link_without_mutating_checkout(self): self.assertEqual(checkout_file.read_text(encoding="utf-8"), "checkout bytes\n") self.assertEqual(json.loads(output.read_text(encoding="utf-8")), result) + def test_runner_atomic_output_accepts_near_limit_basename(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + workspace = root / "workspace" + workspace.mkdir() + output = root / ("x" * 242) + output.write_text("previous bytes\n", encoding="utf-8") + result = {"schema_version": 1, "repositories": []} + with mock.patch.object( + run_docs_corpus, "run_corpus", return_value=result + ): + self.assertEqual( + run_docs_corpus.main( + ["--workspace", str(workspace), "--output", str(output)] + ), + 0, + ) + self.assertEqual(json.loads(output.read_text(encoding="utf-8")), result) + def test_prepare_reparse_workspace_cannot_write_outside(self): with tempfile.TemporaryDirectory() as td: original = prepare_docs_corpus.WORKSPACE_ROOT diff --git a/tools/run_docs_corpus.py b/tools/run_docs_corpus.py index 415036f..39af5d9 100644 --- a/tools/run_docs_corpus.py +++ b/tools/run_docs_corpus.py @@ -322,7 +322,7 @@ def _write_output(output, encoded): _assert_no_reparse_components(output) descriptor, temporary_name = tempfile.mkstemp( dir=output.parent, - prefix=f".{output.name}.", + prefix=".docs-corpus-", suffix=".tmp", ) temporary = Path(temporary_name) From 7c3e78bffc0809bb722d67d94413cf1abefea6e2 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 08:48:06 -0400 Subject: [PATCH 18/20] Keep corpus runner imports read-only --- tests/test_docs_evidence.py | 37 +++++++++++++++++++++++++++++++++++++ tools/run_docs_corpus.py | 9 ++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index d899928..f4fab19 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -1757,6 +1757,43 @@ def test_runner_atomic_output_accepts_near_limit_basename(self): ) self.assertEqual(json.loads(output.read_text(encoding="utf-8")), result) + def test_runner_disables_bytecode_before_checker_imports(self): + with tempfile.TemporaryDirectory() as td: + copied_root = Path(td) / "repo" + copied_tools = copied_root / "tools" + copied_tools.mkdir(parents=True) + shutil.copy2(TOOLS / "run_docs_corpus.py", copied_tools) + copied_scripts = copied_root / "skills" / "docs" / "scripts" + shutil.copytree(SCRIPTS, copied_scripts) + for cache in copied_root.rglob("__pycache__"): + shutil.rmtree(cache) + child_env = os.environ.copy() + child_env.pop("PYTHONDONTWRITEBYTECODE", None) + cli = subprocess.run( + [sys.executable, str(copied_tools / "run_docs_corpus.py"), "--help"], + cwd=copied_root, + env=child_env, + capture_output=True, + text=True, + ) + cli_caches = list(copied_root.rglob("__pycache__")) + imported = subprocess.run( + [ + sys.executable, + "-c", + "import sys; sys.path.insert(0, 'tools'); import run_docs_corpus; " + "print(sys.dont_write_bytecode)", + ], + cwd=copied_root, + env=child_env, + capture_output=True, + text=True, + ) + self.assertEqual(cli.returncode, 0, cli.stdout + cli.stderr) + self.assertEqual(cli_caches, []) + self.assertEqual(imported.returncode, 0, imported.stdout + imported.stderr) + self.assertEqual(imported.stdout.strip(), "False") + def test_prepare_reparse_workspace_cannot_write_outside(self): with tempfile.TemporaryDirectory() as td: original = prepare_docs_corpus.WORKSPACE_ROOT diff --git a/tools/run_docs_corpus.py b/tools/run_docs_corpus.py index 39af5d9..ef46045 100644 --- a/tools/run_docs_corpus.py +++ b/tools/run_docs_corpus.py @@ -3,12 +3,16 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import json import os import re import subprocess -import sys import tempfile from pathlib import Path @@ -30,6 +34,9 @@ from _docs_checker.paths import _assert_no_reparse_components, safe_path # noqa: E402 from check import check # noqa: E402 +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + DEFAULT_MANIFEST = ROOT / "evals" / "docs-corpus-v1.json" DEFAULT_WORKSPACE = ROOT / "evals" / "workspace" / "docs-corpus-v1" From 1de041a66762c30531f4ff3c9426b02b4e7ed292 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 09:07:48 -0400 Subject: [PATCH 19/20] Fail closed on comment-only frontmatter titles --- .../docs/scripts/_docs_checker/evidence.py | 25 ++++++++++++++- .../copilot/scripts/_docs_checker/evidence.py | 25 ++++++++++++++- .../cursor/scripts/_docs_checker/evidence.py | 25 ++++++++++++++- .../grok/scripts/_docs_checker/evidence.py | 25 ++++++++++++++- .../docs/scripts/_docs_checker/evidence.py | 25 ++++++++++++++- skills/docs/scripts/_docs_checker/evidence.py | 25 ++++++++++++++- tests/test_docs_evidence.py | 32 +++++++++++++++++++ 7 files changed, 176 insertions(+), 6 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index d51dd66..df5e44d 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -587,6 +587,22 @@ def _markdown_body_lines(text): return None +def _frontmatter_has_comment_only_title(text): + """Identify an unquoted YAML comment where a title scalar would be expected.""" + if not _has_column_zero_frontmatter_opener(text): + return False + source = text.removeprefix("\ufeff") + for index, line in enumerate(re.split(r"\r\n|\r|\n", source)): + if index and not line.startswith((" ", "\t")) and line.rstrip(" \t") in { + "---", + "...", + }: + return False + if index and re.fullmatch(r"title[ \t]*:[ \t]*#.*", line): + return True + return False + + def _comment_remains_open(line, opening, closing, already_open=False): position = 0 if already_open: @@ -1233,8 +1249,15 @@ def observe_entry_orientation(root, entry): ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: + comment_only_title = _frontmatter_has_comment_only_title(text) + if ( + isinstance(title, str) + and "title" not in unresolved_metadata + and not comment_only_title + ): frontmatter_title = evidence_value("completed", bool(title.strip())) + elif isinstance(title, str) and comment_only_title: + frontmatter_title = evidence_value("unavailable") elif metadata.get("status") in {"absent", "measured"}: frontmatter_title = evidence_value("completed", False) else: diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index d51dd66..df5e44d 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -587,6 +587,22 @@ def _markdown_body_lines(text): return None +def _frontmatter_has_comment_only_title(text): + """Identify an unquoted YAML comment where a title scalar would be expected.""" + if not _has_column_zero_frontmatter_opener(text): + return False + source = text.removeprefix("\ufeff") + for index, line in enumerate(re.split(r"\r\n|\r|\n", source)): + if index and not line.startswith((" ", "\t")) and line.rstrip(" \t") in { + "---", + "...", + }: + return False + if index and re.fullmatch(r"title[ \t]*:[ \t]*#.*", line): + return True + return False + + def _comment_remains_open(line, opening, closing, already_open=False): position = 0 if already_open: @@ -1233,8 +1249,15 @@ def observe_entry_orientation(root, entry): ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: + comment_only_title = _frontmatter_has_comment_only_title(text) + if ( + isinstance(title, str) + and "title" not in unresolved_metadata + and not comment_only_title + ): frontmatter_title = evidence_value("completed", bool(title.strip())) + elif isinstance(title, str) and comment_only_title: + frontmatter_title = evidence_value("unavailable") elif metadata.get("status") in {"absent", "measured"}: frontmatter_title = evidence_value("completed", False) else: diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index d51dd66..df5e44d 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -587,6 +587,22 @@ def _markdown_body_lines(text): return None +def _frontmatter_has_comment_only_title(text): + """Identify an unquoted YAML comment where a title scalar would be expected.""" + if not _has_column_zero_frontmatter_opener(text): + return False + source = text.removeprefix("\ufeff") + for index, line in enumerate(re.split(r"\r\n|\r|\n", source)): + if index and not line.startswith((" ", "\t")) and line.rstrip(" \t") in { + "---", + "...", + }: + return False + if index and re.fullmatch(r"title[ \t]*:[ \t]*#.*", line): + return True + return False + + def _comment_remains_open(line, opening, closing, already_open=False): position = 0 if already_open: @@ -1233,8 +1249,15 @@ def observe_entry_orientation(root, entry): ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: + comment_only_title = _frontmatter_has_comment_only_title(text) + if ( + isinstance(title, str) + and "title" not in unresolved_metadata + and not comment_only_title + ): frontmatter_title = evidence_value("completed", bool(title.strip())) + elif isinstance(title, str) and comment_only_title: + frontmatter_title = evidence_value("unavailable") elif metadata.get("status") in {"absent", "measured"}: frontmatter_title = evidence_value("completed", False) else: diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index d51dd66..df5e44d 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -587,6 +587,22 @@ def _markdown_body_lines(text): return None +def _frontmatter_has_comment_only_title(text): + """Identify an unquoted YAML comment where a title scalar would be expected.""" + if not _has_column_zero_frontmatter_opener(text): + return False + source = text.removeprefix("\ufeff") + for index, line in enumerate(re.split(r"\r\n|\r|\n", source)): + if index and not line.startswith((" ", "\t")) and line.rstrip(" \t") in { + "---", + "...", + }: + return False + if index and re.fullmatch(r"title[ \t]*:[ \t]*#.*", line): + return True + return False + + def _comment_remains_open(line, opening, closing, already_open=False): position = 0 if already_open: @@ -1233,8 +1249,15 @@ def observe_entry_orientation(root, entry): ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: + comment_only_title = _frontmatter_has_comment_only_title(text) + if ( + isinstance(title, str) + and "title" not in unresolved_metadata + and not comment_only_title + ): frontmatter_title = evidence_value("completed", bool(title.strip())) + elif isinstance(title, str) and comment_only_title: + frontmatter_title = evidence_value("unavailable") elif metadata.get("status") in {"absent", "measured"}: frontmatter_title = evidence_value("completed", False) else: diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index d51dd66..df5e44d 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -587,6 +587,22 @@ def _markdown_body_lines(text): return None +def _frontmatter_has_comment_only_title(text): + """Identify an unquoted YAML comment where a title scalar would be expected.""" + if not _has_column_zero_frontmatter_opener(text): + return False + source = text.removeprefix("\ufeff") + for index, line in enumerate(re.split(r"\r\n|\r|\n", source)): + if index and not line.startswith((" ", "\t")) and line.rstrip(" \t") in { + "---", + "...", + }: + return False + if index and re.fullmatch(r"title[ \t]*:[ \t]*#.*", line): + return True + return False + + def _comment_remains_open(line, opening, closing, already_open=False): position = 0 if already_open: @@ -1233,8 +1249,15 @@ def observe_entry_orientation(root, entry): ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: + comment_only_title = _frontmatter_has_comment_only_title(text) + if ( + isinstance(title, str) + and "title" not in unresolved_metadata + and not comment_only_title + ): frontmatter_title = evidence_value("completed", bool(title.strip())) + elif isinstance(title, str) and comment_only_title: + frontmatter_title = evidence_value("unavailable") elif metadata.get("status") in {"absent", "measured"}: frontmatter_title = evidence_value("completed", False) else: diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index d51dd66..df5e44d 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -587,6 +587,22 @@ def _markdown_body_lines(text): return None +def _frontmatter_has_comment_only_title(text): + """Identify an unquoted YAML comment where a title scalar would be expected.""" + if not _has_column_zero_frontmatter_opener(text): + return False + source = text.removeprefix("\ufeff") + for index, line in enumerate(re.split(r"\r\n|\r|\n", source)): + if index and not line.startswith((" ", "\t")) and line.rstrip(" \t") in { + "---", + "...", + }: + return False + if index and re.fullmatch(r"title[ \t]*:[ \t]*#.*", line): + return True + return False + + def _comment_remains_open(line, opening, closing, already_open=False): position = 0 if already_open: @@ -1233,8 +1249,15 @@ def observe_entry_orientation(root, entry): ) title = metadata.get("values", {}).get("title") unresolved_metadata = set(metadata.get("unresolved", ())) - if isinstance(title, str) and "title" not in unresolved_metadata: + comment_only_title = _frontmatter_has_comment_only_title(text) + if ( + isinstance(title, str) + and "title" not in unresolved_metadata + and not comment_only_title + ): frontmatter_title = evidence_value("completed", bool(title.strip())) + elif isinstance(title, str) and comment_only_title: + frontmatter_title = evidence_value("unavailable") elif metadata.get("status") in {"absent", "measured"}: frontmatter_title = evidence_value("completed", False) else: diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index f4fab19..3fb406d 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -524,6 +524,38 @@ def test_orientation_skips_heading_shaped_frontmatter(self): {"status": "unavailable", "value": None}, ) + def test_orientation_marks_comment_only_frontmatter_title_unavailable(self): + scenarios = { + "comment-only": ( + "---\ntitle: # generated by the sidebar\n---\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "spaced-comment-only": ( + "---\ntitle: # generated by the sidebar\n---\n## Start\n", + {"status": "unavailable", "value": None}, + ), + "single-quoted-hash": ( + "---\ntitle: '# literal title'\n---\n## Start\n", + {"status": "completed", "value": True}, + ), + "double-quoted-hash": ( + '---\ntitle: "# literal title"\n---\n## Start\n', + {"status": "completed", "value": True}, + ), + "title-with-comment": ( + "---\ntitle: Useful title # generated by the sidebar\n---\n## Start\n", + {"status": "completed", "value": True}, + ), + } + with tempfile.TemporaryDirectory() as td: + root = Path(td) + for name, (source, expected) in scenarios.items(): + entry = root / f"{name}.mdx" + entry.write_text(source, encoding="utf-8") + observed = evidence.observe_entry_orientation(root, entry.name) + with self.subTest(name=name): + self.assertEqual(observed["frontmatter_title"], expected) + def test_orientation_ignores_comments_and_indented_code(self): scenarios = { "html-comment": "\n## Start\n", From 75c60f2410cb9e171373934675ca0ec5d2b1f6e6 Mon Sep 17 00:00:00 2001 From: Statusnone420 Date: Sat, 18 Jul 2026 09:42:36 -0400 Subject: [PATCH 20/20] Preserve receipt paths and preparer write safety --- .../docs/scripts/_docs_checker/evidence.py | 9 ++- .../copilot/scripts/_docs_checker/evidence.py | 9 ++- .../cursor/scripts/_docs_checker/evidence.py | 9 ++- .../grok/scripts/_docs_checker/evidence.py | 9 ++- .../docs/scripts/_docs_checker/evidence.py | 9 ++- skills/docs/scripts/_docs_checker/evidence.py | 9 ++- tests/test_docs_evidence.py | 67 ++++++++++++++++++- tools/prepare_docs_corpus.py | 9 ++- 8 files changed, 122 insertions(+), 8 deletions(-) diff --git a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py index df5e44d..05038a1 100644 --- a/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/claude/skills/docs/scripts/_docs_checker/evidence.py @@ -1395,7 +1395,14 @@ def build_evidence_receipt( for raw in checker_payload.get("findings", ()): if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): continue - path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + path = next( + ( + raw.get(field) + for field in ("path", "source", "map") + if isinstance(raw.get(field), str) + ), + None, + ) line = raw.get("line") if isinstance(raw.get("line"), int) else None target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) diff --git a/adapters/copilot/scripts/_docs_checker/evidence.py b/adapters/copilot/scripts/_docs_checker/evidence.py index df5e44d..05038a1 100644 --- a/adapters/copilot/scripts/_docs_checker/evidence.py +++ b/adapters/copilot/scripts/_docs_checker/evidence.py @@ -1395,7 +1395,14 @@ def build_evidence_receipt( for raw in checker_payload.get("findings", ()): if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): continue - path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + path = next( + ( + raw.get(field) + for field in ("path", "source", "map") + if isinstance(raw.get(field), str) + ), + None, + ) line = raw.get("line") if isinstance(raw.get("line"), int) else None target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) diff --git a/adapters/cursor/scripts/_docs_checker/evidence.py b/adapters/cursor/scripts/_docs_checker/evidence.py index df5e44d..05038a1 100644 --- a/adapters/cursor/scripts/_docs_checker/evidence.py +++ b/adapters/cursor/scripts/_docs_checker/evidence.py @@ -1395,7 +1395,14 @@ def build_evidence_receipt( for raw in checker_payload.get("findings", ()): if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): continue - path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + path = next( + ( + raw.get(field) + for field in ("path", "source", "map") + if isinstance(raw.get(field), str) + ), + None, + ) line = raw.get("line") if isinstance(raw.get("line"), int) else None target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) diff --git a/adapters/grok/scripts/_docs_checker/evidence.py b/adapters/grok/scripts/_docs_checker/evidence.py index df5e44d..05038a1 100644 --- a/adapters/grok/scripts/_docs_checker/evidence.py +++ b/adapters/grok/scripts/_docs_checker/evidence.py @@ -1395,7 +1395,14 @@ def build_evidence_receipt( for raw in checker_payload.get("findings", ()): if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): continue - path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + path = next( + ( + raw.get(field) + for field in ("path", "source", "map") + if isinstance(raw.get(field), str) + ), + None, + ) line = raw.get("line") if isinstance(raw.get("line"), int) else None target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) diff --git a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py index df5e44d..05038a1 100644 --- a/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py +++ b/adapters/plugin/skills/docs/scripts/_docs_checker/evidence.py @@ -1395,7 +1395,14 @@ def build_evidence_receipt( for raw in checker_payload.get("findings", ()): if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): continue - path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + path = next( + ( + raw.get(field) + for field in ("path", "source", "map") + if isinstance(raw.get(field), str) + ), + None, + ) line = raw.get("line") if isinstance(raw.get("line"), int) else None target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) diff --git a/skills/docs/scripts/_docs_checker/evidence.py b/skills/docs/scripts/_docs_checker/evidence.py index df5e44d..05038a1 100644 --- a/skills/docs/scripts/_docs_checker/evidence.py +++ b/skills/docs/scripts/_docs_checker/evidence.py @@ -1395,7 +1395,14 @@ def build_evidence_receipt( for raw in checker_payload.get("findings", ()): if not isinstance(raw, Mapping) or not isinstance(raw.get("kind"), str): continue - path = raw.get("path") if isinstance(raw.get("path"), str) else raw.get("source") + path = next( + ( + raw.get(field) + for field in ("path", "source", "map") + if isinstance(raw.get(field), str) + ), + None, + ) line = raw.get("line") if isinstance(raw.get("line"), int) else None target = raw.get("target") if isinstance(raw.get("target"), str) else None findings.append(finding_receipt(raw["kind"], path=path, line=line, target=target)) diff --git a/tests/test_docs_evidence.py b/tests/test_docs_evidence.py index 3fb406d..60e2278 100644 --- a/tests/test_docs_evidence.py +++ b/tests/test_docs_evidence.py @@ -292,7 +292,7 @@ def test_completed_health_requires_every_category(self): with self.assertRaisesRegex(ValueError, "exceeds available"): evidence.validate_evidence_receipt(value) - def test_builder_counts_hidden_pages_separately_from_pages(self): + def test_builder_preserves_counts_and_missing_map_path(self): with tempfile.TemporaryDirectory() as td: root = Path(td) docs = root / "docs" @@ -329,6 +329,22 @@ def test_builder_counts_hidden_pages_separately_from_pages(self): "semantic": run_docs_corpus._semantic_not_assessed(), } receipt = evidence.build_evidence_receipt(**builder) + missing_map_builder = copy.deepcopy(builder) + missing_map_builder["checker_payload"]["findings"] = [ + {"kind": "missing-map", "map": "docs/missing-map.md"}, + { + "kind": "missing-map", + "source": "docs/source.md", + "map": "docs/lower-priority-map.md", + }, + { + "kind": "missing-map", + "path": "docs/path.md", + "source": "docs/lower-priority-source.md", + "map": "docs/lower-priority-map.md", + }, + ] + missing_map_receipt = evidence.build_evidence_receipt(**missing_map_builder) for field, malformed in ( ("run", None), @@ -354,6 +370,17 @@ def test_builder_counts_hidden_pages_separately_from_pages(self): evidence.build_evidence_receipt(**malformed_builder) self.assertEqual(receipt["counts"]["pages"], {"status": "completed", "value": 2}) self.assertEqual(receipt["counts"]["hidden_pages"], {"status": "completed", "value": 1}) + self.assertEqual( + missing_map_receipt["evidence"]["deterministic"]["findings"][0]["path"], + {"status": "completed", "value": "docs/missing-map.md"}, + ) + self.assertEqual( + [ + row["path"]["value"] + for row in missing_map_receipt["evidence"]["deterministic"]["findings"] + ], + ["docs/missing-map.md", "docs/source.md", "docs/path.md"], + ) def test_unavailable_is_not_zero_and_must_match_index(self): with self.assertRaises(ValueError): @@ -1826,6 +1853,44 @@ def test_runner_disables_bytecode_before_checker_imports(self): self.assertEqual(imported.returncode, 0, imported.stdout + imported.stderr) self.assertEqual(imported.stdout.strip(), "False") + def test_preparer_disables_bytecode_before_runner_imports(self): + with tempfile.TemporaryDirectory() as td: + copied_root = Path(td) / "repo" + copied_tools = copied_root / "tools" + copied_tools.mkdir(parents=True) + shutil.copy2(TOOLS / "prepare_docs_corpus.py", copied_tools) + shutil.copy2(TOOLS / "run_docs_corpus.py", copied_tools) + copied_scripts = copied_root / "skills" / "docs" / "scripts" + shutil.copytree(SCRIPTS, copied_scripts) + for cache in copied_root.rglob("__pycache__"): + shutil.rmtree(cache) + child_env = os.environ.copy() + child_env.pop("PYTHONDONTWRITEBYTECODE", None) + cli = subprocess.run( + [sys.executable, str(copied_tools / "prepare_docs_corpus.py"), "--help"], + cwd=copied_root, + env=child_env, + capture_output=True, + text=True, + ) + cli_caches = list(copied_root.rglob("__pycache__")) + imported = subprocess.run( + [ + sys.executable, + "-c", + "import sys; sys.path.insert(0, 'tools'); import prepare_docs_corpus; " + "print(sys.dont_write_bytecode)", + ], + cwd=copied_root, + env=child_env, + capture_output=True, + text=True, + ) + self.assertEqual(cli.returncode, 0, cli.stdout + cli.stderr) + self.assertEqual(cli_caches, []) + self.assertEqual(imported.returncode, 0, imported.stdout + imported.stderr) + self.assertEqual(imported.stdout.strip(), "False") + def test_prepare_reparse_workspace_cannot_write_outside(self): with tempfile.TemporaryDirectory() as td: original = prepare_docs_corpus.WORKSPACE_ROOT diff --git a/tools/prepare_docs_corpus.py b/tools/prepare_docs_corpus.py index 3e4c62f..8ab9d0c 100644 --- a/tools/prepare_docs_corpus.py +++ b/tools/prepare_docs_corpus.py @@ -3,11 +3,15 @@ from __future__ import annotations +import sys + +_previous_dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True + import argparse import hashlib import json import subprocess -import sys from pathlib import Path from run_docs_corpus import ( @@ -21,6 +25,9 @@ verify_checkout, ) +sys.dont_write_bytecode = _previous_dont_write_bytecode +del _previous_dont_write_bytecode + WORKSPACE_ROOT = ROOT / "evals" / "workspace" MARKER = ".docs-corpus-owner.json"