Initial LinuxCNC WebAssembly workspace snapshot

This commit is contained in:
2026-08-20 18:21:15 +08:00
commit 5dd614c335
92 changed files with 200203 additions and 0 deletions

16
scripts/build-wasm.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
build_dir="${repo_root}/build-wasm"
native_build_dir="${repo_root}/build-native"
emcmake cmake -S "${repo_root}/wasm-core" -B "${build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build "${build_dir}"
cmake -S "${repo_root}/wasm-core" -B "${native_build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build "${native_build_dir}"
ctest --test-dir "${native_build_dir}" --output-on-failure
printf 'wasm_build=%s\n' "${build_dir}/linuxcnc-sim-core.js"
printf 'native_test=%s\n' "${native_build_dir}/sim_api_test"
printf 'native_protocol_test=%s\n' "${native_build_dir}/sim_protocol_test"

315
scripts/context_packet.py Normal file
View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""Generate bounded, auditable task context packets.
The selector applies byte and count limits before writing a packet. It never
creates an over-budget packet for a later validator to reject.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
PROFILES = {
"doc": {"tokens": 2048, "bytes": 8192, "evidence_bytes": 2048, "files": 8},
"feature": {"tokens": 4096, "bytes": 16384, "evidence_bytes": 6144, "files": 16},
"contract": {"tokens": 6144, "bytes": 24576, "evidence_bytes": 10240, "files": 24},
"milestone": {"tokens": 8192, "bytes": 32768, "evidence_bytes": 14336, "files": 32},
}
SEGMENTS = ("header", "baseline", "scope", "contracts", "evidence", "risks", "next", "forbidden")
# Compact codes keep the audit trail inside the packet budget. The human
# explanation is stable and can be rendered by the reader without repeating
# large strings in every packet.
EXCLUSION_REASONS = {
"E1": "duplicate_path",
"E2": "evidence_item_bytes_exceed_budget",
"E3": "evidence_file_count_exceed_budget",
"E4": "evidence_bytes_exceed_budget",
"E5": "context_remaining_bytes_insufficient",
}
class PacketError(ValueError):
"""Raised when a packet cannot be generated without violating a budget."""
@dataclass(frozen=True)
class Candidate:
path: str
size: int
sha256: str | None
kind: str
priority: int
required: bool
def _json_bytes(value: Any) -> bytes:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _candidate(raw: dict[str, Any], base_dir: Path) -> Candidate:
path_value = str(raw.get("path", ""))
if not path_value:
raise PacketError("candidate is missing path")
path = Path(path_value)
if "size" in raw:
size = int(raw["size"])
else:
file_path = path if path.is_absolute() else base_dir / path
try:
size = file_path.stat().st_size
except OSError as exc:
raise PacketError(f"cannot stat evidence {path_value}: {exc}") from exc
if size < 0:
raise PacketError(f"evidence size must be non-negative: {path_value}")
sha256 = raw.get("sha256")
if sha256 is None and raw.get("hash_file", False):
file_path = path if path.is_absolute() else base_dir / path
sha256 = _sha256_file(file_path)
return Candidate(
path=path_value,
size=size,
sha256=str(sha256) if sha256 is not None else None,
kind=str(raw.get("kind", "artifact")),
priority=int(raw.get("priority", 0)),
required=bool(raw.get("required", False)),
)
def _evidence_item(candidate: Candidate) -> dict[str, Any]:
item: dict[str, Any] = {"path": candidate.path, "bytes": candidate.size, "kind": candidate.kind}
if candidate.sha256:
item["sha256"] = candidate.sha256
return item
def _packet_bytes(packet: dict[str, Any]) -> int:
return len(_json_bytes(packet))
def _context_bytes(packet: dict[str, Any], evidence_used: int) -> int:
"""Return the bytes consumed by the envelope and referenced evidence.
Evidence is referenced by path in the JSON envelope, but the referenced
payload is read into the task context by the consumer. Counting both
here prevents a packet that is small on disk from expanding past the
context limit when it is loaded.
"""
return _packet_bytes(packet) + evidence_used
def _finalize_packet_size(packet: dict[str, Any]) -> int:
"""Store the self-consistent serialized size in the budget metadata."""
packet["budget"]["packet_bytes"] = 0
for _ in range(4):
packet_bytes = _packet_bytes(packet)
if packet["budget"]["packet_bytes"] == packet_bytes:
return packet_bytes
packet["budget"]["packet_bytes"] = packet_bytes
raise PacketError("could not stabilize serialized packet size")
def _base_packet(input_data: dict[str, Any], context_id: str, packet_id: str) -> dict[str, Any]:
packet: dict[str, Any] = {
"context_id": context_id,
"packet_id": packet_id,
"task": str(input_data.get("task", "")),
"header": dict(input_data.get("header", {})),
"baseline": dict(input_data.get("baseline", {})),
"scope": dict(input_data.get("scope", {})),
"contracts": list(input_data.get("contracts", [])),
"evidence": {"selected": [], "excluded": []},
"risks": list(input_data.get("risks", [])),
"next": list(input_data.get("next", []))[:3],
"forbidden": list(input_data.get("forbidden", [])),
}
return packet
def _budget_metadata(limits: dict[str, int], selected: list[dict[str, Any]], excluded: list[dict[str, Any]], evidence_used: int) -> dict[str, Any]:
return {
"profile": limits["profile"],
"token_limit": limits["tokens"],
"byte_limit": limits["bytes"],
"evidence_byte_limit": limits["evidence_bytes"],
"evidence_file_limit": limits["files"],
"selected_evidence_bytes": evidence_used,
"selected_evidence_files": len(selected),
"excluded_evidence_files": len(excluded),
# Reserve the complete decimal width of the configured limit while
# selecting. This keeps the selection check conservative.
"packet_bytes": limits["bytes"],
}
def generate_packet(
input_data: dict[str, Any],
*,
context_id: str,
packet_id: str,
profile: str = "feature",
total_bytes: int | None = None,
evidence_bytes: int | None = None,
max_files: int | None = None,
base_dir: Path = Path("."),
) -> dict[str, Any]:
"""Select evidence under all budgets and return a packet within its limit."""
if profile not in PROFILES:
raise PacketError(f"unknown profile: {profile}")
limits = dict(PROFILES[profile])
limits["profile"] = profile
if total_bytes is not None:
limits["bytes"] = int(total_bytes)
if evidence_bytes is not None:
limits["evidence_bytes"] = int(evidence_bytes)
if max_files is not None:
limits["files"] = int(max_files)
if any(limits[key] < 0 for key in ("tokens", "bytes", "evidence_bytes", "files")):
raise PacketError("budgets must be non-negative")
raw_candidates = input_data.get("candidates", [])
if not isinstance(raw_candidates, list):
raise PacketError("candidates must be a list")
candidates = []
for raw in raw_candidates:
if not isinstance(raw, dict):
raise PacketError("each candidate must be an object")
candidates.append(_candidate(raw, base_dir))
candidates.sort(key=lambda item: (-int(item.required), -item.priority, item.path))
packet = _base_packet(input_data, context_id, packet_id)
skeleton_bytes = _packet_bytes(packet)
if skeleton_bytes > limits["bytes"]:
raise PacketError(
f"context skeleton exceeds byte budget ({skeleton_bytes}>{limits['bytes']}); "
"reduce task metadata or use a larger profile"
)
selected: list[dict[str, Any]] = []
excluded: list[dict[str, Any]] = []
evidence_used = 0
seen: set[str] = set()
for candidate in candidates:
if candidate.path in seen:
excluded.append({"path": candidate.path, "reason_code": "E1"})
continue
seen.add(candidate.path)
item = _evidence_item(candidate)
reason: str | None = None
if candidate.size > limits["evidence_bytes"]:
reason = "E2"
elif len(selected) >= limits["files"]:
reason = "E3"
elif evidence_used + candidate.size > limits["evidence_bytes"]:
reason = "E4"
else:
trial = dict(packet)
trial_selected = selected + [item]
trial["evidence"] = {"selected": trial_selected, "excluded": excluded}
trial["budget"] = _budget_metadata(
limits, trial_selected, excluded, evidence_used + candidate.size
)
# This check is intentionally performed before selection is committed.
trial_context_bytes = _context_bytes(trial, evidence_used + candidate.size)
if trial_context_bytes > limits["bytes"]:
reason = "E5"
if reason:
excluded.append({"path": candidate.path, "reason_code": reason})
if candidate.required:
raise PacketError(f"required evidence cannot fit ({reason}): {candidate.path}")
continue
selected.append(item)
evidence_used += candidate.size
# The exclusion audit trail is part of the packet, so it can consume the
# remaining context space. Remove optional selections until the complete
# packet (selected evidence, exclusions and budget metadata) fits.
while True:
packet["evidence"] = {"selected": selected, "excluded": excluded}
packet["budget"] = _budget_metadata(limits, selected, excluded, evidence_used)
packet_bytes = _finalize_packet_size(packet)
context_bytes = packet_bytes + evidence_used
if packet_bytes <= limits["bytes"] and context_bytes <= limits["bytes"]:
break
removable_index = next(
(index for index in range(len(selected) - 1, -1, -1)
if not any(item["path"] == selected[index]["path"] and candidate.required
for candidate in candidates)),
None,
)
if removable_index is None:
raise PacketError("context skeleton and required evidence exceed byte budget")
removed = selected.pop(removable_index)
evidence_used -= int(removed["bytes"])
excluded.append({
"path": removed["path"],
"reason_code": "E5",
})
return packet
def _load(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise PacketError("input must be a JSON object")
return value
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", required=True, type=Path, help="candidate/context JSON")
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--context-id", required=True)
parser.add_argument("--packet-id", required=True)
parser.add_argument("--profile", choices=sorted(PROFILES), default="feature")
parser.add_argument("--base-dir", type=Path, default=Path("."))
parser.add_argument("--total-bytes", type=int)
parser.add_argument("--evidence-bytes", type=int)
parser.add_argument("--max-files", type=int)
args = parser.parse_args(list(argv) if argv is not None else None)
try:
packet = generate_packet(
_load(args.input),
context_id=args.context_id,
packet_id=args.packet_id,
profile=args.profile,
total_bytes=args.total_bytes,
evidence_bytes=args.evidence_bytes,
max_files=args.max_files,
base_dir=args.base_dir,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
payload = _json_bytes(packet)
# Do not expose a partially written packet to a concurrent validator.
temporary = args.output.with_name(f".{args.output.name}.tmp")
temporary.write_bytes(payload)
if len(payload) != packet["budget"]["packet_bytes"]:
raise PacketError("serialized packet size changed before write")
temporary.replace(args.output)
except (PacketError, OSError, json.JSONDecodeError) as exc:
print(f"context packet generation failed: {exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""Generate resource dependency graphs for every LinuxCNC simulation INI."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
from typing import Iterable
CONFIG_ROOT = Path("linuxcnc/configs/sim")
RESOURCE_EXTENSIONS = {
".ini": "ini", ".hal": "hal", ".tbl": "tool_table", ".var": "parameter_file",
".ngc": "gcode", ".nc": "gcode", ".tap": "gcode", ".py": "python",
".xml": "ui_descriptor", ".ui": "ui_descriptor", ".glade": "ui_descriptor",
".tcl": "script", ".clp": "classicladder", ".pref": "preference",
".prefs": "preference", ".dat": "data", ".jpg": "image", ".jpeg": "image",
".png": "image", ".gif": "image", ".stl": "model", ".obj": "model",
}
KEY_KINDS = {
"HALFILE": "hal", "POSTGUI_HALFILE": "hal", "TOOL_TABLE": "tool_table",
"PARAMETER_FILE": "parameter_file", "POSITION_FILE": "data", "LOG_FILE": "log",
"MDI_HISTORY_FILE": "data", "PREFERENCE_FILE_PATH": "preference",
"INTRO_GRAPHIC": "image", "PROGRAM_PREFIX": "program_directory",
"SUBROUTINE_PATH": "subroutine_directory", "USER_COMMAND": "user_command",
"SHUTDOWN": "script", "APP": "application",
}
HAL_COMMANDS = {"source": "hal_source", "loadusr": "hal_user_component"}
TOKEN_RE = re.compile(r"(?<![A-Za-z0-9_./~-])([^\s\"'=;,]+)")
EXT_RE = re.compile(r"\.(?:ini|hal|tbl|var|ngc|nc|tap|py|xml|ui|glade|tcl|clp|pref|prefs|dat|png|jpg|jpeg|gif|stl|obj)$", re.I)
_SHA_CACHE: dict[Path, str] = {}
def sha256(path: Path) -> str:
path = path.resolve()
cached = _SHA_CACHE.get(path)
if cached:
return cached
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
value = digest.hexdigest()
_SHA_CACHE[path] = value
return value
def rel(root: Path, path: Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def clean_value(value: str) -> str:
value = value.split("#", 1)[0].split(";", 1)[0].strip().strip('"').strip("'")
return value
def parse_ini(path: Path) -> list[tuple[str, str, int]]:
section = ""
rows: list[tuple[str, str, int]] = []
for line_number, raw in enumerate(path.read_text(encoding="utf-8", errors="strict").splitlines(), 1):
line = raw.strip()
if not line or line.startswith(("#", ";")):
continue
if line.startswith("[") and "]" in line:
section = line[1:line.index("]")].strip().upper()
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
rows.append((section, key.strip().upper(), clean_value(value)))
return rows
def resolve_reference(repo_root: Path, config_root: Path, base: Path, value: str) -> tuple[Path | None, str]:
value = clean_value(value)
if not value:
return None, "empty"
if value.startswith(("~", "$", "${")):
return None, "host_or_variable_path"
candidate = Path(value)
if not candidate.is_absolute():
candidate = base / candidate
candidate = candidate.resolve()
try:
candidate.relative_to(repo_root.resolve())
except ValueError:
return None, "outside_repo"
return candidate, "path"
def tokens_with_extensions(value: str) -> list[str]:
return [match.group(1) for match in TOKEN_RE.finditer(value) if EXT_RE.search(match.group(1))]
def config_files(config_root: Path) -> list[Path]:
return sorted(path for path in config_root.rglob("*") if path.is_file())
def add_row(rows: list[dict[str, object]], repo_root: Path, config_root: Path, entrypoint: Path,
target: Path | None, raw: str, kind: str, edge_kind: str, line: int, status: str | None = None,
origin: Path | None = None) -> None:
entry_id = rel(repo_root, entrypoint)
if target is None:
target_path = raw
target_sha = None
target_status = status or "unresolved"
else:
target_path = rel(repo_root, target) if target.exists() else rel(repo_root, target)
target_sha = sha256(target) if target.is_file() else None
target_status = "resolved_file" if target.is_file() else ("resolved_directory" if target.is_dir() else "missing")
rows.append({
"resource_source_id": f"resource:{entry_id}:{line}:{edge_kind}:{target_path}",
"entrypoint": entry_id,
"path": target_path,
"resource_kind": kind,
"edge_kind": edge_kind,
"line": line,
"raw_value": raw,
"source_sha": sha256(origin or entrypoint),
"target_sha256": target_sha,
"origin_path": rel(repo_root, origin or entrypoint),
"parse_status": target_status,
})
def scan_entrypoint(repo_root: Path, config_root: Path, entrypoint: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
ini_rows = parse_ini(entrypoint)
row_numbers = {(section, key, value): index for index, (section, key, value) in enumerate(ini_rows, 1)}
for section, key, value in ini_rows:
if key in KEY_KINDS:
kind = KEY_KINDS[key]
if key in {"SUBROUTINE_PATH"}:
values = value.split(":")
elif key == "PROGRAM_PREFIX":
values = [value]
else:
values = [value]
for item in values:
target, status = resolve_reference(repo_root, config_root, entrypoint.parent, item)
add_row(rows, repo_root, config_root, entrypoint, target, item, kind, f"ini:{section}:{key}", row_numbers[(section, key, value)], status if target is None else None)
elif section == "FILTER" and key not in {"PROGRAM_EXTENSION"}:
for token in tokens_with_extensions(value):
target, status = resolve_reference(repo_root, config_root, entrypoint.parent, token)
add_row(rows, repo_root, config_root, entrypoint, target, token, "filter", "ini:filter_command", row_numbers[(section, key, value)], status if target is None else None)
elif key in {"EMBED_TAB_COMMAND", "DISPLAY", "HALCMD", "TASK", "EMCIO"}:
for token in tokens_with_extensions(value):
target, status = resolve_reference(repo_root, config_root, entrypoint.parent, token)
add_row(rows, repo_root, config_root, entrypoint, target, token, RESOURCE_EXTENSIONS.get(Path(token).suffix.lower(), "external"), f"ini:{section}:{key}", row_numbers[(section, key, value)], status if target is None else None)
seen_paths = {str(row["path"]) for row in rows}
for path in sorted(path for path in entrypoint.parent.rglob("*") if path.is_file() and path != entrypoint):
path_key = rel(repo_root, path)
if path.suffix.lower() in RESOURCE_EXTENSIONS and path_key not in seen_paths:
add_row(rows, repo_root, config_root, entrypoint, path, rel(repo_root, path), RESOURCE_EXTENSIONS[path.suffix.lower()], "directory_member", 0)
seen_paths.add(path_key)
seen_ids = {str(existing["resource_source_id"]) for existing in rows}
for row in parse_hal_references(repo_root, config_root, entrypoint, rows):
if row["resource_source_id"] not in seen_ids:
rows.append(row)
seen_ids.add(str(row["resource_source_id"]))
return rows
def parse_hal_references(repo_root: Path, config_root: Path, entrypoint: Path, rows: list[dict[str, object]]) -> list[dict[str, object]]:
output: list[dict[str, object]] = []
hal_paths = [Path(str(row["path"])) for row in rows if row["resource_kind"] == "hal" and row["parse_status"] == "resolved_file"]
for relative_path in hal_paths:
hal = repo_root / relative_path
for line_number, raw in enumerate(hal.read_text(encoding="utf-8", errors="strict").splitlines(), 1):
parts = raw.strip().split()
if not parts or parts[0] not in HAL_COMMANDS:
continue
command = parts[0]
candidates = tokens_with_extensions(raw)
for token in candidates:
target, status = resolve_reference(repo_root, config_root, hal.parent, token)
add_row(output, repo_root, config_root, entrypoint, target, token, RESOURCE_EXTENSIONS.get(Path(token).suffix.lower(), "external"), f"hal:{command}", line_number, status if target is None else None, hal)
return output
def generate(repo_root: Path, output: Path, report_path: Path) -> tuple[int, str]:
config_root = repo_root / CONFIG_ROOT
if not config_root.is_dir():
raise FileNotFoundError(f"required config root is missing: {CONFIG_ROOT}")
entrypoints = sorted(path for path in config_root.rglob("*.ini") if path.is_file())
rows: list[dict[str, object]] = []
for entrypoint in entrypoints:
rows.extend(scan_entrypoint(repo_root, config_root, entrypoint))
rows.sort(key=lambda row: str(row["resource_source_id"]))
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="\n") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
digest = sha256(output)
report = {
"schema": "config-resources-report.v1",
"entrypoints": len(entrypoints),
"resources": len(rows),
"missing": sum(1 for row in rows if row["parse_status"] == "missing"),
"unresolved": sum(1 for row in rows if row["parse_status"] in {"unresolved", "host_or_variable_path", "outside_repo"}),
"output_sha256": digest,
"entrypoint_paths": [rel(repo_root, path) for path in entrypoints],
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return len(rows), digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/config-resources.jsonl"))
parser.add_argument("--report", type=Path, default=Path("coverage/config-resources.report.json"))
args = parser.parse_args()
root = args.repo_root.resolve()
count, digest = generate(root, root / args.output, root / args.report)
print(f"config_resources={count} sha256={digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Build a deterministic dependency graph from normalized coverage records."""
from __future__ import annotations
import argparse
import hashlib
import json
from collections import Counter, defaultdict
from pathlib import Path
NORMALIZED = Path("coverage/normalized-sources.jsonl")
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def read_jsonl(path: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
with path.open("r", encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
if not line.strip():
continue
row = json.loads(line)
if not isinstance(row, dict):
raise ValueError(f"{path}:{line_number}: expected object")
rows.append(row)
return rows
def canonical_path(value: object) -> str | None:
if not isinstance(value, str) or not value.strip():
return None
raw = value.replace("\\", "/").strip()
if raw.startswith("/"):
return None
parts: list[str] = []
for part in raw.split("/"):
if part in ("", "."):
continue
if part == "..":
if not parts:
return None
parts.pop()
else:
parts.append(part)
return "/".join(parts) or None
def generate(root: Path, output: Path, report_path: Path) -> tuple[int, str]:
normalized_path = root / NORMALIZED
if not normalized_path.is_file():
raise FileNotFoundError(f"required normalized index is missing: {NORMALIZED}")
records = read_jsonl(normalized_path)
file_ids = {
str(row["path"]): str(row["source_id"])
for row in records if row.get("origin_index") == "source" and row.get("path")
}
nodes = {str(row["source_id"]) for row in records}
edges: list[dict[str, object]] = []
edge_counts: Counter[str] = Counter()
duplicate_edges: list[dict[str, object]] = []
unresolved: list[dict[str, object]] = []
def target_for(path_value: object, status: str = "unresolved") -> tuple[str, str]:
path = canonical_path(path_value)
if path and path in file_ids:
return file_ids[path], "resolved"
if path and (root / path).is_file():
return f"unindexed:{path}", "unindexed"
return f"unresolved:{path or str(path_value) or 'empty'}", status
def add_edge(from_id: str, to_id: str, edge_kind: str, record: dict[str, object], status: str, raw_value: object = None) -> None:
key = f"{from_id}|{edge_kind}|{to_id}|{record.get('line', '')}"
edge_counts[key] += 1
if edge_counts[key] > 1:
duplicate_edges.append({"key": key, "occurrence": edge_counts[key], "source_id": record.get("source_id")})
edge_id = f"edge:{key}" + (f"~dup{edge_counts[key]}" if edge_counts[key] > 1 else "")
row = {
"edge_id": edge_id,
"from_source_id": from_id,
"to_source_id": to_id,
"edge_kind": edge_kind,
"raw_value": raw_value,
"evidence_path": record.get("hash_path") or record.get("path"),
"evidence_line": record.get("line"),
"evidence_source_id": record.get("source_id"),
"parse_status": status,
}
edges.append(row)
nodes.add(to_id)
if status not in {"resolved", "parsed"}:
unresolved.append({"edge_id": edge_id, "to_source_id": to_id, "status": status, "raw_value": raw_value})
for record in records:
source_id = str(record["source_id"])
index_name = str(record.get("origin_index"))
if index_name == "resource":
origin_path = canonical_path(record.get("hash_path"))
from_id = file_ids.get(origin_path, source_id)
target_id, status = target_for(record.get("path"), str(record.get("target_status") or "unresolved"))
add_edge(from_id, target_id, f"resource:{record.get('edge_kind') or record.get('resource_kind') or 'reference'}", record, status, record.get("raw_value"))
elif index_name == "test":
entry_path = canonical_path(record.get("path"))
entry_file = file_ids.get(entry_path)
if entry_file:
add_edge(entry_file, source_id, "test_entrypoint", record, "resolved", record.get("command"))
for field, edge_kind in (("inputs", "test_input"), ("expected", "test_expected"), ("config_refs", "test_config_ref")):
values = record.get(field, [])
if not isinstance(values, list):
continue
for value in sorted(str(item) for item in values):
target_id, status = target_for(value)
add_edge(source_id, target_id, edge_kind, record, status, value)
elif index_name in {"symbol", "document"}:
related = record.get("related_source_ids", [])
if isinstance(related, list):
for related_id in sorted(str(item) for item in related):
add_edge(related_id, source_id, "declares" if index_name == "symbol" else "documents", record, "resolved", record.get("path"))
edges.sort(key=lambda row: str(row["edge_id"]))
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="\n") as handle:
for row in edges:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
digest = sha256(output)
incoming = Counter(str(edge["to_source_id"]) for edge in edges)
outgoing = Counter(str(edge["from_source_id"]) for edge in edges)
orphan_nodes = sorted(node for node in nodes if not incoming[node] and not outgoing[node] and not node.startswith(("unresolved:", "unindexed:")))
report = {
"schema": "dependency-closure-report.v1",
"normalized_input_sha256": sha256(normalized_path),
"nodes": len(nodes),
"edges": len(edges),
"resolved_edges": sum(1 for edge in edges if edge["parse_status"] == "resolved"),
"unresolved_edges": len(unresolved),
"edge_kinds": dict(sorted(Counter(str(edge["edge_kind"]) for edge in edges).items())),
"duplicate_edges": duplicate_edges,
"unresolved": unresolved,
"orphan_nodes": orphan_nodes,
"output_sha256": digest,
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return len(edges), digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/dependency-closure.jsonl"))
parser.add_argument("--report", type=Path, default=Path("coverage/dependency-closure.report.json"))
args = parser.parse_args()
root = args.repo_root.resolve()
count, digest = generate(root, root / args.output, root / args.report)
print(f"dependency_edges={count} sha256={digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""Generate a deterministic, source-addressable LinuxCNC document index.
The index is intentionally a source index, not a coverage claim. Every
heading in the Master Documentation include graph and every heading in the
project's migration documents becomes a planned capability candidate. Files
that cannot be classified are retained with ``needs_review`` rather than
silently discarded.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
MASTER = Path("linuxcnc/docs/src/Master_Documentation.adoc")
PROJECT_DOCS = (
Path("docs/linuxcnc-web-wasm-source-map.md"),
Path("docs/linuxcnc-web-wasm-simulation-coverage.md"),
)
INCLUDE_RE = re.compile(r"^\s*include::([^\[]+)\[.*\]\s*$")
ASCIIDOC_ANCHOR_RE = re.compile(r"^\s*\[\[([^,\]]+)(?:,[^\]]*)?\]\]\s*$")
ASCIIDOC_BLOCK_ID_RE = re.compile(r"^\s*\[#([^\]]+)\]\s*$")
ASCIIDOC_HEADING_RE = re.compile(r"^(={1,6})\s+(.+?)\s*$")
MARKDOWN_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$")
SLUG_RE = re.compile(r"[^a-z0-9]+")
DOMAIN_RULES = (
("getting-started", "getting_started"),
("user", "user"),
("config", "configuration"),
("hal", "hal"),
("ladder", "classicladder"),
("motion", "motion"),
("gcode", "gcode"),
("remap", "remap"),
("gui", "gui"),
("plasma", "plasma"),
("tooldatabase", "tooldata"),
("drivers", "real_hardware"),
("examples", "examples"),
("integrator", "integrator"),
("install", "installation"),
("source-highlight", "documentation"),
)
@dataclass(frozen=True)
class Heading:
level: int
title: str
anchor: str | None
line: int
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def slugify(title: str) -> str:
value = title.lower().replace("&", " and ")
value = SLUG_RE.sub("-", value).strip("-")
return value or "section"
def parse_headings(path: Path) -> list[Heading]:
lines = path.read_text(encoding="utf-8", errors="strict").splitlines()
headings: list[Heading] = []
pending_anchor: str | None = None
for line_number, line in enumerate(lines, 1):
anchor_match = ASCIIDOC_ANCHOR_RE.match(line) or ASCIIDOC_BLOCK_ID_RE.match(line)
if anchor_match:
pending_anchor = anchor_match.group(1).strip()
continue
match = ASCIIDOC_HEADING_RE.match(line) or MARKDOWN_HEADING_RE.match(line)
if not match:
continue
headings.append(Heading(len(match.group(1)), match.group(2).strip(), pending_anchor, line_number))
pending_anchor = None
return headings
def include_graph(repo_root: Path, master: Path = MASTER) -> tuple[list[Path], list[dict[str, str]]]:
ordered: list[Path] = []
seen: set[Path] = set()
missing: list[dict[str, str]] = []
def visit(path: Path) -> None:
path = path.resolve()
if path in seen:
return
if not path.is_file():
raise FileNotFoundError(f"included documentation file is missing: {path}")
seen.add(path)
ordered.append(path)
for line in path.read_text(encoding="utf-8", errors="strict").splitlines():
match = INCLUDE_RE.match(line)
if match:
include_path = (path.parent / match.group(1)).resolve()
if not include_path.is_file():
missing.append({
"document": relative_path(repo_root, path),
"include": match.group(1),
"resolved_path": relative_path(repo_root, include_path),
})
continue
visit(include_path)
visit((repo_root / master).resolve())
return ordered, missing
def classify(path: Path, title: str) -> tuple[str, str]:
lowered = "/".join(path.parts).lower()
for token, domain in DOMAIN_RULES:
if token in lowered:
return domain, "mapped"
title_lower = title.lower()
if any(token in title_lower for token in ("g-code", "m-code", "o-code", "rs274")):
return "gcode", "mapped"
if any(token in title_lower for token in ("hardware", "driver", "mesa", "gpio")):
return "real_hardware", "needs_review"
if path.suffix.lower() in {".md", ".adoc"} and path.as_posix().startswith("docs/"):
return "web_wasm_governance", "mapped"
return "unclassified", "needs_review"
def relative_path(repo_root: Path, path: Path) -> str:
return path.resolve().relative_to(repo_root.resolve()).as_posix()
def make_entries(repo_root: Path) -> tuple[list[dict[str, object]], dict[str, object]]:
paths, missing_includes = include_graph(repo_root)
for project_doc in PROJECT_DOCS:
path = (repo_root / project_doc).resolve()
if not path.is_file():
raise FileNotFoundError(f"project migration document is missing: {project_doc}")
if path not in paths:
paths.append(path)
entries: list[dict[str, object]] = []
report: dict[str, object] = {
"schema": "document-capabilities-report.v1",
"documents": len(paths),
"headings": 0,
"unclassified": [],
"duplicate_ids": [],
"empty_documents": [],
"missing_includes": missing_includes,
}
used_ids: set[str] = set()
for path in paths:
relative = relative_path(repo_root, path)
document_sha = sha256(path)
headings = parse_headings(path)
if not headings:
report["empty_documents"].append(relative)
headings = [Heading(1, path.stem, None, 1)]
anchor_counts: dict[str, int] = {}
parent_stack: list[tuple[int, str]] = []
for heading in headings:
base_anchor = heading.anchor or slugify(heading.title)
anchor_counts[base_anchor] = anchor_counts.get(base_anchor, 0) + 1
anchor = base_anchor if anchor_counts[base_anchor] == 1 else f"{base_anchor}-{anchor_counts[base_anchor]}"
capability_id = f"doc:{relative}#{anchor}"
if capability_id in used_ids:
report["duplicate_ids"].append(capability_id)
continue
used_ids.add(capability_id)
while parent_stack and parent_stack[-1][0] >= heading.level:
parent_stack.pop()
parent_anchor = parent_stack[-1][1] if parent_stack else None
parent_stack.append((heading.level, anchor))
domain, mapping_status = classify(path, heading.title)
entry = {
"capability_source_id": capability_id,
"document": relative,
"anchor": anchor,
"title": heading.title,
"domain": domain,
"source_sha": document_sha,
"line": heading.line,
"level": heading.level,
"parent_anchor": parent_anchor,
"mapping_status": mapping_status,
"sim_status": "planned",
"web_status": "planned",
}
entries.append(entry)
report["headings"] += 1
if mapping_status != "mapped":
report["unclassified"].append({"capability_source_id": capability_id, "domain": domain})
entries.sort(key=lambda item: str(item["capability_source_id"]))
return entries, report
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
def write_jsonl(path: Path, entries: Iterable[dict[str, object]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="\n") as handle:
for entry in entries:
handle.write(json.dumps(entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")))
handle.write("\n")
def generate(repo_root: Path, output: Path, report_path: Path) -> tuple[int, str]:
entries, report = make_entries(repo_root)
write_jsonl(output, entries)
report["output_sha256"] = sha256(output)
write_json(report_path, report)
return len(entries), str(report["output_sha256"])
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/document-capabilities.jsonl"))
parser.add_argument("--report", type=Path, default=Path("coverage/document-capabilities.report.json"))
args = parser.parse_args()
root = args.repo_root.resolve()
count, digest = generate(root, root / args.output, root / args.report)
print(f"document_capabilities={count} sha256={digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Generate the reproducible LinuxCNC source and coverage seed indexes.
The source index is deliberately mechanical: every file in a declared input
root is recorded with its size and digest. The coverage seed is not an
implementation claim; entries remain ``planned`` until an owner assigns a
feature, adapter, test and oracle.
"""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from typing import Iterable
ROOTS = (
("doc", Path("linuxcnc/docs")),
("source", Path("linuxcnc/src")),
("config", Path("linuxcnc/configs/sim")),
("test", Path("linuxcnc/tests")),
("unit_test", Path("linuxcnc/unit_tests")),
("implementation", Path("wasm-core")),
("implementation", Path("web")),
)
BUILD_FILES = (
Path("linuxcnc/VERSION"),
Path("linuxcnc/meson.build"),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def source_kind(root_kind: str, path: Path) -> str:
if root_kind == "implementation":
if path.suffix in {".h", ".hpp"}:
return "web_wasm_interface"
if path.suffix in {".c", ".cc", ".cpp", ".mjs", ".js"}:
return "web_wasm_implementation"
return "web_wasm_build_or_test"
if root_kind == "doc":
return "documentation"
if root_kind in {"test", "unit_test"}:
return "test"
if root_kind == "config":
return "simulation_config"
if path.name.endswith(".comp"):
return "hal_component_source"
return "source"
def iter_files(repo_root: Path) -> Iterable[tuple[str, Path]]:
for root_kind, relative_root in ROOTS:
root = repo_root / relative_root
if not root.is_dir():
raise FileNotFoundError(f"required inventory root is missing: {relative_root}")
for path in sorted(path for path in root.rglob("*") if path.is_file()):
yield root_kind, path
for relative_path in BUILD_FILES:
path = repo_root / relative_path
if not path.is_file():
raise FileNotFoundError(f"required build source is missing: {relative_path}")
yield "build", path
def make_entry(repo_root: Path, root_kind: str, path: Path) -> dict[str, object]:
relative = path.relative_to(repo_root).as_posix()
source_id = f"{root_kind}:{relative}"
return {
"source_id": source_id,
"path": relative,
"kind": source_kind(root_kind, path),
"bytes": path.stat().st_size,
"sha256": sha256(path),
}
def make_manifest_entry(source: dict[str, object]) -> dict[str, object]:
source_id = str(source["source_id"])
return {
"id": f"seed:{source_id}",
"feature_id": None,
"source": source_id,
"source_sha": source["sha256"],
"domain": "unassigned",
"kind": source["kind"],
"adaptation": None,
"dependencies": [],
"inputs": [source["path"]],
"outputs": [],
"errors": [],
"sim_status": "planned",
"web_status": "planned",
"test_id": None,
"oracle": None,
"golden": None,
"difference": None,
"coverage_dimensions": [],
"context_id": None,
"exclusion_reason": None,
"mapping_status": "needs_owner",
}
def write_jsonl(path: Path, entries: Iterable[dict[str, object]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="\n") as handle:
for entry in entries:
handle.write(json.dumps(entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")))
handle.write("\n")
def generate(repo_root: Path, source_index: Path, manifest: Path) -> tuple[int, int]:
sources = [make_entry(repo_root, kind, path) for kind, path in iter_files(repo_root)]
sources.sort(key=lambda item: str(item["source_id"]))
write_jsonl(source_index, sources)
write_jsonl(manifest, (make_manifest_entry(source) for source in sources))
return len(sources), len(sources)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--source-index", type=Path, default=Path("coverage/source-index.jsonl"))
parser.add_argument("--manifest", type=Path, default=Path("coverage/manifest.jsonl"))
args = parser.parse_args()
source_count, manifest_count = generate(
args.repo_root.resolve(),
args.repo_root.resolve() / args.source_index,
args.repo_root.resolve() / args.manifest,
)
print(f"source_index={source_count} manifest_seed={manifest_count}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""Generate deterministic source symbol and HAL component entry indexes."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
from typing import Iterable
ROOTS = (
Path("linuxcnc/src/emc"),
Path("linuxcnc/src/hal"),
Path("linuxcnc/lib/python"),
)
EXTENSIONS = {
".c": "c", ".h": "c", ".cc": "cpp", ".hh": "cpp", ".cpp": "cpp",
".hpp": "cpp", ".cxx": "cpp", ".hxx": "cpp", ".py": "python", ".comp": "hal_comp",
}
IDENT = r"[A-Za-z_][A-Za-z0-9_]*"
KEYWORDS = {"if", "for", "while", "switch", "catch", "sizeof", "return", "do"}
DEFINE_RE = re.compile(rf"^\s*#\s*define\s+({IDENT})\b")
C_FUNCTION_RE = re.compile(
rf"^\s*(?:(?:static|inline|extern|const|volatile|unsigned|signed|long|short|struct|class|enum|[A-Za-z_][\w:*&<>]*)\s+)+({IDENT})\s*\([^;{{}}]*\)\s*(?:const\s*)?(?:\{{.*|;)?\s*$"
)
C_TYPE_RE = re.compile(rf"^\s*(?:typedef\s+)?(?:struct|class|enum|union)\s+({IDENT})\b")
PY_DEF_RE = re.compile(rf"^\s*(async\s+)?def\s+({IDENT})\s*\(")
PY_CLASS_RE = re.compile(rf"^\s*class\s+({IDENT})\b")
COMP_PATTERNS = (
("component", re.compile(rf"^\s*component\s+({IDENT})\b")),
("function", re.compile(rf"^\s*function\s+({IDENT})\b")),
("option", re.compile(rf"^\s*option\s+({IDENT})\b")),
("pin", re.compile(rf"^\s*pin\s+(?:in|out|io)\s+{IDENT}\s+({IDENT})\b")),
("param", re.compile(rf"^\s*param\s+(?:r|rw)\s+{IDENT}\s+({IDENT})\b")),
("variable", re.compile(rf"^\s*variable\s+{IDENT}\s+({IDENT})\b")),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def files(repo_root: Path) -> Iterable[Path]:
for root in ROOTS:
absolute = repo_root / root
if not absolute.is_dir():
raise FileNotFoundError(f"required symbol root is missing: {root}")
for path in sorted(path for path in absolute.rglob("*") if path.is_file() and path.suffix.lower() in EXTENSIONS):
yield path
def relative(repo_root: Path, path: Path) -> str:
return path.resolve().relative_to(repo_root.resolve()).as_posix()
def add_symbol(rows: list[dict[str, object]], repo_root: Path, path: Path, language: str, symbol: str,
symbol_kind: str, line: int, source_sha: str, **extra: object) -> None:
rows.append({
"symbol_source_id": f"symbol:{relative(repo_root, path)}:{line}:{symbol_kind}:{symbol}",
"path": relative(repo_root, path),
"language": language,
"symbol": symbol,
"symbol_kind": symbol_kind,
"line": line,
"source_sha": source_sha,
"parse_status": "parsed",
**extra,
})
def parse_file(repo_root: Path, path: Path) -> tuple[list[dict[str, object]], dict[str, object]]:
language = EXTENSIONS[path.suffix.lower()]
source_sha = sha256(path)
rows: list[dict[str, object]] = []
report: dict[str, object] = {"path": relative(repo_root, path), "language": language, "symbols": 0, "parse_status": "parsed"}
try:
lines = path.read_text(encoding="utf-8", errors="strict").splitlines()
except UnicodeDecodeError as exc:
report.update({"parse_status": "decode_error", "error": str(exc)})
return rows, report
for line_number, line in enumerate(lines, 1):
match = DEFINE_RE.match(line) if language in {"c", "cpp"} else None
if match:
add_symbol(rows, repo_root, path, language, match.group(1), "macro", line_number, source_sha)
continue
if language == "python":
match = PY_DEF_RE.match(line) or PY_CLASS_RE.match(line)
if match:
symbol = match.group(2) if match.re is PY_DEF_RE else match.group(1)
kind = "async_function" if match.re is PY_DEF_RE and match.group(1) else ("function" if match.re is PY_DEF_RE else "class")
add_symbol(rows, repo_root, path, language, symbol, kind, line_number, source_sha)
continue
if language == "hal_comp":
for kind, pattern in COMP_PATTERNS:
match = pattern.match(line)
if match:
add_symbol(rows, repo_root, path, language, match.group(1), kind, line_number, source_sha)
break
continue
match = C_TYPE_RE.match(line)
if match:
add_symbol(rows, repo_root, path, language, match.group(1), "type", line_number, source_sha)
continue
match = C_FUNCTION_RE.match(line)
if match:
symbol = match.group(1)
if symbol not in KEYWORDS:
add_symbol(rows, repo_root, path, language, symbol, "function", line_number, source_sha)
report["symbols"] = len(rows)
return rows, report
def generate(repo_root: Path, output: Path, report_path: Path) -> tuple[int, str]:
rows: list[dict[str, object]] = []
file_reports: list[dict[str, object]] = []
for path in files(repo_root):
parsed, report = parse_file(repo_root, path)
rows.extend(parsed)
file_reports.append(report)
rows.sort(key=lambda row: str(row["symbol_source_id"]))
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="\n") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
digest = sha256(output)
report = {
"schema": "source-symbols-report.v1",
"files": len(file_reports),
"symbols": len(rows),
"files_without_symbols": sorted(report["path"] for report in file_reports if report["symbols"] == 0),
"decode_errors": [report for report in file_reports if report["parse_status"] != "parsed"],
"output_sha256": digest,
"file_reports": file_reports,
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return len(rows), digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/source-symbols.jsonl"))
parser.add_argument("--report", type=Path, default=Path("coverage/source-symbols.report.json"))
args = parser.parse_args()
count, digest = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.output, args.repo_root.resolve() / args.report)
print(f"source_symbols={count} sha256={digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Generate a deterministic index of LinuxCNC test and unit-test entrypoints."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from collections import Counter
from pathlib import Path
TEST_ROOT = Path("linuxcnc/tests")
UNIT_ROOT = Path("linuxcnc/unit_tests")
MESON_PATHS = (Path("linuxcnc/meson.build"),)
ENTRY_NAMES = {"test.sh", "test.py", "runstreamer", "checkresult"}
HARDWARE_TOKENS = (
"/dev/", "hm2", "hostmot2", "mesa", "parport", "gpio", "pci", "modbus",
"serial", "rtai", "xenomai", "latency", "linuxcncrsh", "mqtt", "tcp", "usb",
)
REALTIME_TOKENS = ("$realtime", "realtime", "rtapi", "halcmd", "halrun", "loadrt")
CONFIG_SUFFIXES = {".ini", ".hal", ".tbl", ".var", ".ngc", ".nc", ".tap", ".py", ".tcl"}
CONFIG_TOKEN_RE = re.compile(r"(?<![A-Za-z0-9_./~-])([^\s\"'=;,]+)")
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def rel(root: Path, path: Path) -> str:
# Keep symlinked/shared test paths distinct; hashing still follows the link.
return path.absolute().relative_to(root.absolute()).as_posix()
def read_text(path: Path) -> tuple[str | None, str | None]:
try:
return path.read_text(encoding="utf-8", errors="strict"), None
except (OSError, UnicodeError) as exc:
return None, f"{type(exc).__name__}: {exc}"
def classify(path: Path, text: str) -> tuple[str, str]:
haystack = f"{path.as_posix()}\n{text}".lower()
if any(token in haystack for token in HARDWARE_TOKENS):
return "REAL-EXCLUDED", "hardware_or_external_transport"
if any(token in haystack for token in REALTIME_TOKENS):
return "needs_review", "realtime_or_linuxcnc_process_dependency"
return "portable_candidate", "simulated_or_library_only"
def directory_context(root: Path, path: Path) -> tuple[list[str], list[str]]:
directory = path.parent
files = sorted(candidate for candidate in directory.iterdir() if candidate.is_file())
inputs = [rel(root, candidate) for candidate in files if candidate != path]
expected = [
rel(root, candidate) for candidate in files
if candidate.name.lower().startswith(("expected", "result", "golden"))
or candidate.name.lower() in {"expected", "expected.txt", "result", "golden"}
]
return inputs, expected
def config_refs(root: Path, path: Path, text: str) -> list[str]:
refs: set[str] = set()
for token in CONFIG_TOKEN_RE.findall(text):
if Path(token).suffix.lower() not in CONFIG_SUFFIXES:
continue
candidate = (path.parent / token).resolve()
try:
candidate.relative_to(root.resolve())
except ValueError:
continue
refs.add(rel(root, candidate))
return sorted(refs)
def entry(root: Path, path: Path, test_kind: str, command: str) -> dict[str, object]:
text, error = read_text(path)
if text is None:
text = ""
portability, reason = classify(path, text)
inputs, expected = directory_context(root, path)
status = "blocked" if error else "parsed"
row: dict[str, object] = {
"test_source_id": f"test:{rel(root, path)}",
"path": rel(root, path),
"test_kind": test_kind,
"command": command,
"inputs": inputs,
"expected": expected,
"config_refs": config_refs(root, path, text),
"portability": portability,
"portability_reason": reason,
"source_sha": sha256(path) if error is None else None,
"parse_status": status,
}
if error:
row["parse_error"] = error
return row
def script_command(root: Path, path: Path) -> tuple[str, str]:
name = path.name
if name == "test.py":
return "python_test", f"python3 {rel(root, path)}"
if name == "runstreamer":
return "stream_test", f"bash {rel(root, path)}"
if name == "checkresult":
text, _ = read_text(path)
interpreter = "python3" if text and "python" in text.splitlines()[0].lower() else "bash"
return "oracle_check", f"{interpreter} {rel(root, path)}"
return "shell_test", f"bash {rel(root, path)}"
def meson_entries(root: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
paths = [root / path for path in MESON_PATHS]
paths.extend(sorted((root / UNIT_ROOT).rglob("meson.build")))
for path in sorted(set(paths)):
if not path.is_file():
continue
text, error = read_text(path)
if text is None:
text = ""
portability, reason = classify(path, text)
source_path = rel(root, path)
targets = sorted(set(re.findall(r"\btest\s*\(\s*['\"]([^'\"]+)", text)))
targets.extend(sorted(set(re.findall(r"executable\s*\(\s*['\"](test_[^'\"]+)", text))))
target_names = sorted(set(targets))
command = "meson test -C build-native --suite unit_tests"
row: dict[str, object] = {
"test_source_id": f"test:{source_path}",
"path": source_path,
"test_kind": "unit_test_build",
"command": command,
"inputs": sorted(rel(root, child) for child in path.parent.iterdir() if child.is_file()),
"expected": [],
"config_refs": [],
"meson_targets": target_names,
"portability": portability,
"portability_reason": reason,
"source_sha": sha256(path) if error is None else None,
"parse_status": "blocked" if error else ("parsed" if target_names else "needs_review"),
}
if error:
row["parse_error"] = error
rows.append(row)
return rows
def unit_source_entries(root: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
if not (root / UNIT_ROOT).is_dir():
return rows
for path in sorted((root / UNIT_ROOT).rglob("*")):
if not path.is_file() or path.suffix.lower() not in {".c", ".cc", ".cpp", ".cxx"}:
continue
if not (path.name.startswith("test_") or path.name.startswith("tests_")):
continue
source_path = rel(root, path)
portability, reason = classify(path, read_text(path)[0] or "")
rows.append({
"test_source_id": f"test:{source_path}",
"path": source_path,
"test_kind": "unit_test_source",
"command": f"meson test -C build-native {path.stem.removeprefix('test_')}",
"inputs": [source_path],
"expected": [],
"config_refs": [],
"portability": portability,
"portability_reason": reason,
"source_sha": sha256(path),
"parse_status": "parsed",
})
return rows
def generate(root: Path, output: Path, report_path: Path) -> tuple[int, str]:
roots = [root / TEST_ROOT, root / UNIT_ROOT]
rows: list[dict[str, object]] = []
parse_errors: list[dict[str, str]] = []
for scan_root in roots:
if not scan_root.is_dir():
continue
for path in sorted(scan_root.rglob("*")):
if not path.is_file() or path.name not in ENTRY_NAMES:
continue
kind, command = script_command(root, path)
row = entry(root, path, kind, command)
rows.append(row)
if row["parse_status"] == "blocked":
parse_errors.append({"path": str(row["path"]), "error": str(row.get("parse_error", ""))})
rows.extend(meson_entries(root))
rows.extend(unit_source_entries(root))
rows.sort(key=lambda row: str(row["test_source_id"]))
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="\n") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
digest = sha256(output)
portability = Counter(str(row["portability"]) for row in rows)
kinds = Counter(str(row["test_kind"]) for row in rows)
report = {
"schema": "test-entries-report.v1",
"entries": len(rows),
"test_directories": len({str(Path(str(row["path"])).parent) for row in rows}),
"test_kinds": dict(sorted(kinds.items())),
"portability": dict(sorted(portability.items())),
"parse_errors": parse_errors,
"blocked": sum(1 for row in rows if row["parse_status"] == "blocked"),
"output_sha256": digest,
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return len(rows), digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/test-entries.jsonl"))
parser.add_argument("--report", type=Path, default=Path("coverage/test-entries.report.json"))
args = parser.parse_args()
root = args.repo_root.resolve()
count, digest = generate(root, root / args.output, root / args.report)
print(f"test_entries={count} sha256={digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

243
scripts/native_runner.py Normal file
View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""Run one indexed test in a fixed native environment and emit an oracle record."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import time
from typing import Any
SCHEMA = "native-runner-result.v1"
GOLDEN_SCHEMA = "native-golden.v1"
DEFAULT_INDEX = Path("coverage/test-entries.jsonl")
DEFAULT_REPORT = Path("coverage/native-runner.report.json")
DEFAULT_ARTIFACTS = Path("build-native/native-runner-artifacts")
MAX_OUTPUT_BYTES = 4 * 1024 * 1024
PORTABLE_ENV = {
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME": ".runner-home",
"LANG": "C",
"LC_ALL": "C",
"TZ": "UTC",
"PYTHONHASHSEED": "0",
}
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def read_entries(path: Path) -> list[dict[str, object]]:
entries: list[dict[str, object]] = []
with path.open("r", encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
if not line.strip():
continue
row = json.loads(line)
if not isinstance(row, dict):
raise ValueError(f"{path}:{line_number}: expected object")
entries.append(row)
return entries
def environment(root: Path) -> dict[str, str]:
result = dict(PORTABLE_ENV)
result["PWD"] = str(root)
result["LINUXCNC_RUNNER_ROOT"] = str(root)
return result
def command_for_workdir(root: Path, entry: dict[str, object], workdir: Path) -> list[str]:
command = entry.get("command")
if not isinstance(command, str) or not command.strip():
raise ValueError("entry has no executable command")
tokens = shlex.split(command)
entry_path = root / str(entry.get("path", ""))
for index, token in enumerate(tokens):
candidate = (root / token).resolve() if not token.startswith("/") else Path(token)
try:
candidate.relative_to(workdir.resolve())
except ValueError:
continue
if candidate.is_file():
tokens[index] = os.path.relpath(candidate, workdir)
if entry_path.is_file() and tokens and tokens[-1] == str(entry.get("path")):
tokens[-1] = entry_path.name
return tokens
def result_digest(result: dict[str, object]) -> str:
payload = {key: value for key, value in result.items() if key not in {"duration_ms", "stdout", "stderr"}}
return sha256_bytes(canonical_json(payload).encode("utf-8"))
def compare_golden(actual: dict[str, object], golden: dict[str, object]) -> dict[str, object]:
if golden.get("schema") != GOLDEN_SCHEMA:
return {"status": "blocked", "reason": "unsupported_golden_schema"}
expected = golden.get("expected", {})
if not isinstance(expected, dict):
return {"status": "blocked", "reason": "golden_expected_must_be_object"}
differences: list[dict[str, object]] = []
for field, expected_value in sorted(expected.items()):
if actual.get(field) != expected_value:
differences.append({"field": field, "expected": expected_value, "actual": actual.get(field)})
if differences:
return {"status": "different", "differences": differences}
return {"status": "passed", "differences": []}
def run_entry(root: Path, entry: dict[str, object], artifacts_dir: Path, timeout: float,
allow_real_excluded: bool = False, allow_needs_review: bool = False,
golden: dict[str, object] | None = None) -> dict[str, object]:
portability = str(entry.get("portability", "needs_review"))
if portability == "REAL-EXCLUDED" and not allow_real_excluded:
return {
"schema": SCHEMA, "status": "REAL-EXCLUDED", "test_source_id": entry.get("test_source_id"),
"path": entry.get("path"), "portability": portability, "reason": "hardware_or_external_transport_requires_substitute",
}
if portability == "needs_review" and not allow_needs_review:
return {
"schema": SCHEMA, "status": "blocked", "test_source_id": entry.get("test_source_id"),
"path": entry.get("path"), "portability": portability, "reason": "needs_review_requires_explicit_override",
}
if entry.get("parse_status") == "blocked":
return {"schema": SCHEMA, "status": "blocked", "test_source_id": entry.get("test_source_id"), "path": entry.get("path"), "reason": "entry_parse_blocked"}
entry_path = root / str(entry.get("path", ""))
workdir = entry_path.parent if entry_path.is_file() else root
try:
command = command_for_workdir(root, entry, workdir)
except (ValueError, OSError) as exc:
return {"schema": SCHEMA, "status": "blocked", "test_source_id": entry.get("test_source_id"), "path": entry.get("path"), "reason": str(exc)}
try:
completed = subprocess.run(
command,
cwd=workdir,
env=environment(root),
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
)
timed_out = False
except subprocess.TimeoutExpired as exc:
completed = subprocess.CompletedProcess(command, 124, exc.stdout or b"", exc.stderr or b"")
timed_out = True
stdout = bytes(completed.stdout or b"")
stderr = bytes(completed.stderr or b"")
artifact_root = artifacts_dir / str(entry.get("test_source_id", "unknown")).replace("/", "_").replace(":", "_")
artifact_root.mkdir(parents=True, exist_ok=True)
stdout_path = artifact_root / "stdout.bin"
stderr_path = artifact_root / "stderr.bin"
stdout_path.write_bytes(stdout[:MAX_OUTPUT_BYTES])
stderr_path.write_bytes(stderr[:MAX_OUTPUT_BYTES])
status = "timeout" if timed_out else ("passed" if completed.returncode == 0 else "failed")
result: dict[str, object] = {
"schema": SCHEMA,
"status": status,
"test_source_id": entry.get("test_source_id"),
"path": entry.get("path"),
"command": command,
"cwd": str(workdir.relative_to(root)),
"portability": portability,
"exit_code": completed.returncode,
"stdout_bytes": len(stdout),
"stderr_bytes": len(stderr),
"stdout_sha256": sha256_bytes(stdout),
"stderr_sha256": sha256_bytes(stderr),
"stdout_artifact": str(stdout_path.relative_to(root)),
"stderr_artifact": str(stderr_path.relative_to(root)),
"source_sha": entry.get("source_sha"),
}
if len(stdout) <= MAX_OUTPUT_BYTES:
result["stdout"] = stdout.decode("utf-8", errors="replace")
if len(stderr) <= MAX_OUTPUT_BYTES:
result["stderr"] = stderr.decode("utf-8", errors="replace")
result["result_sha256"] = result_digest(result)
if golden is not None:
result["oracle"] = compare_golden(result, golden)
return result
def self_test(root: Path, report_path: Path) -> int:
completed = subprocess.run(
[sys.executable, "-c", "print('native-runner-self-test')"],
cwd=root,
env=environment(root),
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
passed = completed.returncode == 0 and completed.stdout == b"native-runner-self-test\n" and completed.stderr == b""
report = {
"schema": "native-runner-report.v1",
"status": "passed" if passed else "failed",
"self_test": True,
"result": {
"exit_code": completed.returncode,
"stdout_sha256": sha256_bytes(completed.stdout),
"stderr_sha256": sha256_bytes(completed.stderr),
"expected_stdout_sha256": sha256_bytes(b"native-runner-self-test\n"),
},
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
print(f"native_runner=self-test status={report['status']}")
return 0 if passed else 1
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--index", type=Path, default=DEFAULT_INDEX)
parser.add_argument("--test-source-id")
parser.add_argument("--path")
parser.add_argument("--timeout", type=float, default=60.0)
parser.add_argument("--allow-real-excluded", action="store_true")
parser.add_argument("--allow-needs-review", action="store_true")
parser.add_argument("--golden", type=Path)
parser.add_argument("--artifacts-dir", type=Path, default=DEFAULT_ARTIFACTS)
parser.add_argument("--report", type=Path, default=DEFAULT_REPORT)
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
root = args.repo_root.resolve()
if args.self_test:
return self_test(root, root / args.report)
entries = read_entries(root / args.index)
matches = [entry for entry in entries if (not args.test_source_id or entry.get("test_source_id") == args.test_source_id) and (not args.path or entry.get("path") == args.path)]
if len(matches) != 1:
parser.error(f"expected exactly one test entry, found {len(matches)}")
golden = None
if args.golden:
golden = json.loads((root / args.golden).read_text(encoding="utf-8"))
result = run_entry(root, matches[0], root / args.artifacts_dir, args.timeout, args.allow_real_excluded, args.allow_needs_review, golden)
report = {"schema": "native-runner-report.v1", "status": result.get("status"), "result": result}
(root / args.report).parent.mkdir(parents=True, exist_ok=True)
(root / args.report).write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
print(canonical_json(result))
return 0 if result.get("status") == "passed" else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Normalize the independent coverage indexes into stable cross-index records."""
from __future__ import annotations
import argparse
import hashlib
import json
from collections import defaultdict
from pathlib import Path, PurePosixPath
from typing import Iterable
INDEXES = (
("source", Path("coverage/source-index.jsonl")),
("document", Path("coverage/document-capabilities.jsonl")),
("symbol", Path("coverage/source-symbols.jsonl")),
("resource", Path("coverage/config-resources.jsonl")),
("test", Path("coverage/test-entries.jsonl")),
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def read_jsonl(path: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
with path.open("r", encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
if not line.strip():
continue
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number}: expected object")
rows.append(value)
return rows
def canonical_path(value: object) -> str | None:
if not isinstance(value, str) or not value.strip():
return None
raw = value.replace("\\", "/").strip()
if raw.startswith("/"):
return None
parts: list[str] = []
for part in PurePosixPath(raw).parts:
if part in ("", "."):
continue
if part == "..":
if not parts:
return None
parts.pop()
continue
parts.append(part)
return "/".join(parts) or None
def as_text(value: object) -> str:
return str(value) if value is not None else ""
def source_key(index_name: str, row: dict[str, object], path: str | None) -> str:
if index_name == "source":
return f"src:file:{path or as_text(row.get('source_id'))}"
if index_name == "document":
return f"src:doc:{path or 'unknown'}#{as_text(row.get('anchor')) or 'root'}"
if index_name == "symbol":
return "src:symbol:{}:{}:{}:{}".format(
path or "unknown", as_text(row.get("line")), as_text(row.get("symbol_kind")), as_text(row.get("symbol")),
)
if index_name == "resource":
entrypoint = canonical_path(row.get("entrypoint")) or "unknown"
return f"src:resource:{entrypoint}:{as_text(row.get('line'))}:{as_text(row.get('edge_kind'))}:{path or as_text(row.get('raw_value'))}"
return f"src:test:{path or as_text(row.get('test_source_id'))}"
def row_path(index_name: str, row: dict[str, object]) -> str | None:
if index_name == "document":
return canonical_path(row.get("document"))
if index_name == "resource":
return canonical_path(row.get("path"))
return canonical_path(row.get("path"))
def hash_path(index_name: str, row: dict[str, object]) -> str | None:
if index_name == "resource":
return canonical_path(row.get("origin_path")) or canonical_path(row.get("entrypoint"))
return row_path(index_name, row)
def original_id(index_name: str, row: dict[str, object]) -> str:
keys = {
"source": "source_id",
"document": "capability_source_id",
"symbol": "symbol_source_id",
"resource": "resource_source_id",
"test": "test_source_id",
}
return as_text(row.get(keys[index_name]))
def normalize_row(index_name: str, row: dict[str, object], root: Path, path_to_file_id: dict[str, str]) -> dict[str, object]:
path = row_path(index_name, row)
source_id = source_key(index_name, row, path)
source_hash = row.get("source_sha", row.get("sha256"))
if source_hash is not None:
source_hash = as_text(source_hash)
related: set[str] = set()
if path in path_to_file_id:
related.add(path_to_file_id[path])
if index_name == "resource":
origin = canonical_path(row.get("origin_path"))
target = canonical_path(row.get("path"))
if origin in path_to_file_id:
related.add(path_to_file_id[origin])
if target in path_to_file_id:
related.add(path_to_file_id[target])
elif index_name == "test":
for ref in row.get("config_refs", []) if isinstance(row.get("config_refs"), list) else []:
ref_path = canonical_path(ref)
if ref_path in path_to_file_id:
related.add(path_to_file_id[ref_path])
record: dict[str, object] = {
"source_id": source_id,
"origin_index": index_name,
"origin_source_id": original_id(index_name, row),
"path": path,
"hash_path": hash_path(index_name, row),
"source_sha": source_hash,
"domain": as_text(row.get("domain")) or ({"source": "source", "document": "documentation", "symbol": "code", "resource": "configuration", "test": "test"}[index_name]),
"kind": as_text(row.get("kind")) or as_text(row.get("symbol_kind")) or as_text(row.get("resource_kind")) or as_text(row.get("test_kind")) or index_name,
"line": row.get("line"),
"parse_status": as_text(row.get("parse_status")) or as_text(row.get("mapping_status")) or "parsed",
"related_source_ids": sorted(related),
}
if index_name == "resource":
record.update({
"edge_kind": row.get("edge_kind"),
"raw_value": row.get("raw_value"),
"resource_kind": row.get("resource_kind"),
"target_status": row.get("parse_status"),
})
elif index_name == "test":
record.update({
"command": row.get("command"),
"inputs": row.get("inputs", []),
"expected": row.get("expected", []),
"config_refs": row.get("config_refs", []),
"test_kind": row.get("test_kind"),
})
return record
def generate(root: Path, output: Path, report_path: Path) -> tuple[int, str]:
loaded: dict[str, list[dict[str, object]]] = {}
input_summary: dict[str, dict[str, object]] = {}
for index_name, relative_path in INDEXES:
path = root / relative_path
if not path.is_file():
raise FileNotFoundError(f"required index is missing: {relative_path}")
loaded[index_name] = read_jsonl(path)
input_summary[index_name] = {
"path": relative_path.as_posix(),
"rows": len(loaded[index_name]),
"sha256": sha256(path),
}
path_to_file_id: dict[str, str] = {}
for row in loaded["source"]:
path = canonical_path(row.get("path"))
if path:
path_to_file_id.setdefault(path, f"src:file:{path}")
records: list[dict[str, object]] = []
source_ids: set[str] = set()
source_id_counts: dict[str, int] = defaultdict(int)
collisions: list[dict[str, object]] = []
path_hashes: dict[str, set[str]] = defaultdict(set)
missing_paths: set[str] = set()
for index_name, _ in INDEXES:
for row in loaded[index_name]:
record = normalize_row(index_name, row, root, path_to_file_id)
base_source_id = str(record["source_id"])
source_id_counts[base_source_id] += 1
if base_source_id in source_ids:
collisions.append({"source_id": base_source_id, "occurrence": source_id_counts[base_source_id], "origin_index": index_name, "origin_source_id": record["origin_source_id"]})
record["source_id"] = f"{base_source_id}~dup{source_id_counts[base_source_id]}"
source_ids.add(str(record["source_id"]))
hash_value = record.get("source_sha")
hash_path_value = record.get("hash_path")
if hash_value and hash_path_value:
path_hashes[str(hash_path_value)].add(str(hash_value))
primary_path = record.get("path")
if primary_path and not (root / str(primary_path)).exists():
if not (index_name == "resource" and as_text(row.get("parse_status")) in {"missing", "unresolved", "host_or_variable_path", "outside_repo"}):
missing_paths.add(str(primary_path))
records.append(record)
records.sort(key=lambda row: str(row["source_id"]))
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="\n") as handle:
for record in records:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
digest = sha256(output)
report = {
"schema": "normalized-sources-report.v1",
"records": len(records),
"unique_source_ids": len(source_ids),
"input_summary": input_summary,
"collisions": collisions,
"hash_conflicts": {path: sorted(values) for path, values in sorted(path_hashes.items()) if len(values) > 1},
"missing_paths": sorted(missing_paths),
"orphan_related_ids": sum(1 for row in records if not row["related_source_ids"] and row["origin_index"] != "source"),
"output_sha256": digest,
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return len(records), digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=Path("coverage/normalized-sources.jsonl"))
parser.add_argument("--report", type=Path, default=Path("coverage/normalized-sources.report.json"))
args = parser.parse_args()
root = args.repo_root.resolve()
count, digest = generate(root, root / args.output, root / args.report)
print(f"normalized_sources={count} sha256={digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_config_resources import generate, scan_entrypoint
class ConfigResourcesTests(unittest.TestCase):
def create_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
config = root / "linuxcnc/configs/sim/demo"
config.mkdir(parents=True)
(config / "main.ini").write_text(
"[HAL]\nHALFILE = core.hal\nPOSTGUI_HALFILE = post.hal\n"
"[RS274NGC]\nPARAMETER_FILE = vars.var\nSUBROUTINE_PATH = macros\n"
"[EMCIO]\nTOOL_TABLE = tools.tbl\n",
encoding="utf-8",
)
(config / "core.hal").write_text("source extra.hal\nloadusr helper.py\n", encoding="utf-8")
(config / "post.hal").write_text("loadrt demo\n", encoding="utf-8")
(config / "extra.hal").write_text("setp demo.value 1\n", encoding="utf-8")
(config / "helper.py").write_text("print('ok')\n", encoding="utf-8")
(config / "tools.tbl").write_text("T1 P1\n", encoding="utf-8")
(config / "vars.var").write_text("#1 = 2\n", encoding="utf-8")
(config / "macros").mkdir()
return directory, root, config / "main.ini"
def test_resolves_ini_and_hal_edges(self):
directory, root, entrypoint = self.create_tree()
self.addCleanup(directory.cleanup)
rows = scan_entrypoint(root, root / "linuxcnc/configs/sim", entrypoint)
paths = {row["path"] for row in rows}
self.assertIn("linuxcnc/configs/sim/demo/core.hal", paths)
self.assertIn("linuxcnc/configs/sim/demo/extra.hal", paths)
self.assertIn("linuxcnc/configs/sim/demo/helper.py", paths)
self.assertIn("linuxcnc/configs/sim/demo/tools.tbl", paths)
def test_missing_resource_is_retained(self):
directory, root, entrypoint = self.create_tree()
self.addCleanup(directory.cleanup)
entrypoint.write_text("[HAL]\nHALFILE = missing.hal\n", encoding="utf-8")
rows = scan_entrypoint(root, root / "linuxcnc/configs/sim", entrypoint)
missing = [row for row in rows if row["path"].endswith("missing.hal")]
self.assertEqual(len(missing), 1)
self.assertEqual(missing[0]["parse_status"], "missing")
def test_generation_is_deterministic(self):
directory, root, _ = self.create_tree()
self.addCleanup(directory.cleanup)
first = root / "one.jsonl"
second = root / "two.jsonl"
report = root / "report.json"
_, digest_one = generate(root, first, report)
_, digest_two = generate(root, second, report)
self.assertEqual(digest_one, digest_two)
self.assertEqual(first.read_bytes(), second.read_bytes())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
import json
import subprocess
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from context_packet import PacketError, generate_packet
class ContextPacketTests(unittest.TestCase):
def base(self, candidates):
return {
"task": "bounded task",
"header": {"milestone": "M1"},
"baseline": {"source_sha": "abc"},
"scope": {"include": ["feature/1"]},
"contracts": ["protocol.v1"],
"risks": [],
"next": ["run test"],
"forbidden": ["real hardware"],
"candidates": candidates,
}
def test_filters_by_evidence_bytes_before_final_write(self):
packet = generate_packet(
self.base([
{"path": "small.log", "size": 6, "priority": 2},
{"path": "large.log", "size": 7, "priority": 1},
]),
context_id="c1",
packet_id="p1",
total_bytes=4096,
evidence_bytes=10,
)
self.assertEqual([item["path"] for item in packet["evidence"]["selected"]], ["small.log"])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E4")
self.assertLessEqual(packet["budget"]["packet_bytes"], 4096)
def test_filters_by_remaining_context_space(self):
packet = generate_packet(
self.base([{"path": "evidence.log", "size": 1, "priority": 1}]),
context_id="c1",
packet_id="p1",
total_bytes=550,
evidence_bytes=100,
)
self.assertEqual(packet["evidence"]["selected"], [])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E5")
self.assertLessEqual(packet["budget"]["packet_bytes"], 550)
def test_evidence_payload_counts_against_total_context_budget(self):
packet = generate_packet(
self.base([{"path": "payload.bin", "size": 40, "priority": 1}]),
context_id="c1",
packet_id="p1",
total_bytes=570,
evidence_bytes=100,
)
self.assertEqual(packet["evidence"]["selected"], [])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E5")
self.assertLessEqual(
packet["budget"]["packet_bytes"]
+ packet["budget"]["selected_evidence_bytes"],
570,
)
self.assertEqual(
packet["budget"]["packet_bytes"],
len(json.dumps(packet, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")),
)
def test_skeleton_over_budget_fails_before_selection(self):
with self.assertRaisesRegex(PacketError, "context skeleton exceeds byte budget"):
generate_packet(
self.base([]),
context_id="c1",
packet_id="p1",
total_bytes=100,
evidence_bytes=100,
)
def test_required_item_fails_without_writing_an_over_budget_packet(self):
with self.assertRaisesRegex(PacketError, "required evidence cannot fit"):
generate_packet(
self.base([{"path": "required.log", "size": 101, "required": True}]),
context_id="c1",
packet_id="p1",
total_bytes=4096,
evidence_bytes=100,
)
def test_required_item_fails_when_payload_consumes_remaining_context(self):
with self.assertRaisesRegex(PacketError, r"required evidence cannot fit \(E5\)"):
generate_packet(
self.base([{"path": "required.log", "size": 40, "required": True}]),
context_id="c1",
packet_id="p1",
total_bytes=570,
evidence_bytes=100,
)
def test_selection_is_deterministic_and_count_is_enforced(self):
packet = generate_packet(
self.base([
{"path": "z.log", "size": 1},
{"path": "a.log", "size": 1},
{"path": "required.log", "size": 1, "required": True},
]),
context_id="c1",
packet_id="p1",
total_bytes=4096,
evidence_bytes=100,
max_files=2,
)
self.assertEqual([item["path"] for item in packet["evidence"]["selected"]], ["required.log", "a.log"])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E3")
def test_cli_does_not_replace_existing_output_on_generation_failure(self):
with TemporaryDirectory() as directory:
root = Path(directory)
input_path = root / "input.json"
output_path = root / "packet.json"
input_path.write_text(
json.dumps(self.base([{"path": "required.log", "size": 101, "required": True}]))
)
output_path.write_text("previous packet")
result = subprocess.run(
[
"python3",
"scripts/context_packet.py",
"--input",
str(input_path),
"--output",
str(output_path),
"--context-id",
"c1",
"--packet-id",
"p1",
"--total-bytes",
"4096",
"--evidence-bytes",
"100",
],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 2)
self.assertEqual(output_path.read_text(), "previous packet")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_dependency_closure import generate
class DependencyClosureTests(unittest.TestCase):
def create_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
(root / "coverage").mkdir()
(root / "linuxcnc").mkdir()
(root / "linuxcnc/src").mkdir()
(root / "linuxcnc/configs").mkdir()
(root / "linuxcnc/tests/demo").mkdir(parents=True)
(root / "linuxcnc/src/a.c").write_text("int a;\n", encoding="utf-8")
(root / "linuxcnc/configs/demo.ini").write_text("[EMC]\n", encoding="utf-8")
(root / "linuxcnc/tests/demo/test.sh").write_text("#!/bin/sh\n", encoding="utf-8")
rows = [
{"source_id": "src:file:linuxcnc/src/a.c", "origin_index": "source", "origin_source_id": "build:a", "path": "linuxcnc/src/a.c", "hash_path": "linuxcnc/src/a.c", "source_sha": "a", "domain": "source", "kind": "source", "line": None, "parse_status": "parsed", "related_source_ids": []},
{"source_id": "src:file:linuxcnc/configs/demo.ini", "origin_index": "source", "origin_source_id": "build:ini", "path": "linuxcnc/configs/demo.ini", "hash_path": "linuxcnc/configs/demo.ini", "source_sha": "b", "domain": "source", "kind": "source", "line": None, "parse_status": "parsed", "related_source_ids": []},
{"source_id": "src:test:linuxcnc/tests/demo/test.sh", "origin_index": "test", "origin_source_id": "test:demo", "path": "linuxcnc/tests/demo/test.sh", "hash_path": "linuxcnc/tests/demo/test.sh", "source_sha": "c", "domain": "test", "kind": "shell_test", "line": None, "parse_status": "parsed", "related_source_ids": [], "command": "bash", "inputs": ["linuxcnc/src/a.c"], "expected": [], "config_refs": ["linuxcnc/configs/missing.ini"], "test_kind": "shell_test"},
]
(root / "coverage/normalized-sources.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
return directory, root
def test_resolved_and_unresolved_edges_are_retained(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
count, _ = generate(root, root / "out.jsonl", root / "report.json")
rows = [json.loads(line) for line in (root / "out.jsonl").read_text(encoding="utf-8").splitlines()]
self.assertEqual(count, 2)
self.assertTrue(any(row["parse_status"] == "resolved" for row in rows))
self.assertTrue(any(row["to_source_id"].startswith("unresolved:") for row in rows))
def test_deterministic_output(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
_, first = generate(root, root / "one.jsonl", root / "report.json")
_, second = generate(root, root / "two.jsonl", root / "report.json")
self.assertEqual(first, second)
self.assertEqual((root / "one.jsonl").read_bytes(), (root / "two.jsonl").read_bytes())
def test_duplicate_edges_are_reported(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
path = root / "coverage/normalized-sources.jsonl"
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
rows[-1]["inputs"] = ["linuxcnc/src/a.c", "linuxcnc/src/a.c"]
path.write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
generate(root, root / "out.jsonl", root / "report.json")
report = json.loads((root / "report.json").read_text(encoding="utf-8"))
self.assertTrue(report["duplicate_edges"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_document_capabilities import make_entries, parse_headings, slugify
class DocumentCapabilitiesTests(unittest.TestCase):
def test_slugify_is_ascii_and_deterministic(self):
self.assertEqual(slugify("G-Code & Motion"), "g-code-and-motion")
self.assertEqual(slugify("!!!"), "section")
def test_explicit_anchor_and_duplicate_heading_are_stable(self):
with TemporaryDirectory() as directory:
path = Path(directory) / "sample.adoc"
path.write_text("[[explicit]]\n== Same\n== Same\n", encoding="utf-8")
headings = parse_headings(path)
self.assertEqual(headings[0].anchor, "explicit")
self.assertEqual(headings[1].anchor, None)
def test_include_graph_and_project_documents_are_indexed(self):
with TemporaryDirectory() as directory:
root = Path(directory)
(root / "linuxcnc/docs/src/sub").mkdir(parents=True)
(root / "docs").mkdir()
(root / "linuxcnc/docs/src/Master_Documentation.adoc").write_text(
"include::sub/a.adoc[]\n", encoding="utf-8"
)
(root / "linuxcnc/docs/src/sub/a.adoc").write_text("[[a]]\n= HAL\n", encoding="utf-8")
(root / "docs/linuxcnc-web-wasm-source-map.md").write_text("# Source Map\n", encoding="utf-8")
(root / "docs/linuxcnc-web-wasm-simulation-coverage.md").write_text("# Coverage\n", encoding="utf-8")
entries, report = make_entries(root)
ids = {str(entry["capability_source_id"]) for entry in entries}
self.assertIn("doc:linuxcnc/docs/src/sub/a.adoc#a", ids)
self.assertIn("doc:docs/linuxcnc-web-wasm-source-map.md#source-map", ids)
self.assertEqual(report["duplicate_ids"], [])
def test_generated_entries_have_required_fields(self):
with TemporaryDirectory() as directory:
root = Path(directory)
(root / "linuxcnc/docs/src").mkdir(parents=True)
(root / "docs").mkdir()
(root / "linuxcnc/docs/src/Master_Documentation.adoc").write_text("= G-Code\n", encoding="utf-8")
for name in ("linuxcnc-web-wasm-source-map.md", "linuxcnc-web-wasm-simulation-coverage.md"):
(root / "docs" / name).write_text("# Project\n", encoding="utf-8")
entries, _ = make_entries(root)
for entry in entries:
self.assertTrue(entry["capability_source_id"])
self.assertTrue(entry["document"])
self.assertTrue(entry["anchor"])
self.assertTrue(entry["title"])
self.assertTrue(entry["source_sha"])
self.assertIn(entry["mapping_status"], {"mapped", "needs_review"})
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import json
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from validate_download_index import validate
class DownloadIndexTests(unittest.TestCase):
def write(self, row):
directory = TemporaryDirectory()
path = Path(directory.name) / "index.jsonl"
path.write_text(json.dumps(row) + "\n", encoding="utf-8")
return directory, path
def test_empty_project_index_is_valid(self):
self.assertEqual(validate(Path("vendor/downloads/index.jsonl")), [])
def test_china_mirror_requires_origin(self):
directory, path = self.write({
"record_type": "artifact",
"name": "demo",
"version": "1",
"source_url": "https://mirror.example/demo",
"sha256": "0" * 64,
"bytes": 1,
"license": "MIT",
"mirror_kind": "china-mirror",
})
self.addCleanup(directory.cleanup)
errors = validate(path)
self.assertTrue(any("origin_url" in error for error in errors))
def test_valid_local_cache_record(self):
directory, path = self.write({
"record_type": "artifact",
"name": "demo",
"version": "1",
"source_url": "demo.tar",
"sha256": "0" * 64,
"bytes": 1,
"license": "MIT",
"mirror_kind": "local-cache",
})
self.addCleanup(directory.cleanup)
self.assertEqual(validate(path), [])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import json
import subprocess
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class HalRuntimeTests(unittest.TestCase):
def test_native_hal_binary(self):
binary = ROOT / "build-native/sim_hal_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
result = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
def test_header_declares_error_boundary(self):
header = (ROOT / "wasm-core/include/sim_hal.h").read_text(encoding="utf-8")
for token in ("SIM_HAL_ERR_TYPE", "SIM_HAL_ERR_FUNCTION", "SIM_HAL_ERR_REAL_EXCLUDED", "sim_hal_step"):
self.assertIn(token, header)
def test_wasm_exports_hal_api(self):
module = ROOT / "build-wasm/linuxcnc-sim-core.js"
if not module.is_file():
self.skipTest("WASM build has not been generated")
text = module.read_text(encoding="utf-8")
for symbol in ("sim_hal_register_pin", "sim_hal_register_thread", "sim_hal_step"):
self.assertIn(symbol, text)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from native_runner import compare_golden, run_entry
class NativeRunnerTests(unittest.TestCase):
def setUp(self):
self.directory = TemporaryDirectory()
self.root = Path(self.directory.name)
(self.root / "linuxcnc/tests/demo").mkdir(parents=True)
(self.root / "linuxcnc/tests/demo/test.sh").write_text("#!/bin/sh\nprintf 'ok\\n'\n", encoding="utf-8")
(self.root / "artifacts").mkdir()
def tearDown(self):
self.directory.cleanup()
def entry(self, portability="portable_candidate"):
return {
"test_source_id": "test:linuxcnc/tests/demo/test.sh",
"path": "linuxcnc/tests/demo/test.sh",
"command": "bash linuxcnc/tests/demo/test.sh",
"portability": portability,
"parse_status": "parsed",
"source_sha": "source",
}
def test_fixed_runner_emits_stable_content_hashes(self):
result = run_entry(self.root, self.entry(), self.root / "artifacts", 5)
self.assertEqual(result["status"], "passed")
self.assertEqual(result["stdout"], "ok\n")
self.assertEqual(result["cwd"], "linuxcnc/tests/demo")
self.assertRegex(result["result_sha256"], r"^[0-9a-f]{64}$")
def test_real_excluded_is_not_executed_without_override(self):
result = run_entry(self.root, self.entry("REAL-EXCLUDED"), self.root / "artifacts", 5)
self.assertEqual(result["status"], "REAL-EXCLUDED")
self.assertNotIn("exit_code", result)
def test_golden_comparison_is_explicit(self):
actual = {"status": "passed", "exit_code": 0}
self.assertEqual(compare_golden(actual, {"schema": "native-golden.v1", "expected": actual})["status"], "passed")
self.assertEqual(compare_golden(actual, {"schema": "native-golden.v1", "expected": {"exit_code": 1}})["status"], "different")
self.assertEqual(compare_golden(actual, {"schema": "unknown", "expected": actual})["status"], "blocked")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from normalize_sources import generate
class NormalizeSourcesTests(unittest.TestCase):
def create_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
(root / "coverage").mkdir()
(root / "linuxcnc/src").mkdir(parents=True)
(root / "docs").mkdir()
(root / "linuxcnc/configs/sim/demo").mkdir(parents=True)
(root / "linuxcnc/tests/demo").mkdir(parents=True)
(root / "linuxcnc/src/demo.c").write_text("int main(void) { return 0; }\n", encoding="utf-8")
(root / "docs/demo.md").write_text("# Demo\n", encoding="utf-8")
(root / "linuxcnc/configs/sim/demo/main.ini").write_text("[EMC]\n", encoding="utf-8")
(root / "linuxcnc/tests/demo/test.sh").write_text("#!/bin/sh\n", encoding="utf-8")
indexes = {
"source-index.jsonl": [{"bytes": 1, "kind": "source", "path": "linuxcnc/src/demo.c", "sha256": "a", "source_id": "build:demo"}],
"document-capabilities.jsonl": [{"anchor": "demo", "capability_source_id": "doc:demo#demo", "document": "docs/demo.md", "domain": "demo", "source_sha": "b", "title": "Demo"}],
"source-symbols.jsonl": [{"language": "c", "line": 1, "parse_status": "parsed", "path": "linuxcnc/src/demo.c", "source_sha": "a", "symbol": "main", "symbol_kind": "function", "symbol_source_id": "symbol:demo"}],
"config-resources.jsonl": [{"edge_kind": "ini:EMC", "entrypoint": "linuxcnc/configs/sim/demo/main.ini", "line": 1, "origin_path": "linuxcnc/configs/sim/demo/main.ini", "parse_status": "resolved_file", "path": "linuxcnc/configs/sim/demo/main.ini", "resource_kind": "ini", "resource_source_id": "resource:demo", "source_sha": "c", "target_sha256": "c"}],
"test-entries.jsonl": [{"command": "bash linuxcnc/tests/demo/test.sh", "config_refs": [], "expected": [], "inputs": [], "parse_status": "parsed", "path": "linuxcnc/tests/demo/test.sh", "portability": "portable_candidate", "source_sha": "d", "test_kind": "shell_test", "test_source_id": "test:demo"}],
}
for name, rows in indexes.items():
(root / "coverage" / name).write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
return directory, root
def test_normalized_ids_are_unique_and_link_files(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
output = root / "coverage/normalized.jsonl"
count, _ = generate(root, output, root / "coverage/report.json")
rows = [json.loads(line) for line in output.read_text(encoding="utf-8").splitlines()]
self.assertEqual(count, 5)
self.assertEqual(len(rows), len({row["source_id"] for row in rows}))
symbol = next(row for row in rows if row["origin_index"] == "symbol")
self.assertIn("src:file:linuxcnc/src/demo.c", symbol["related_source_ids"])
def test_missing_and_hash_conflicts_are_reported(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
source = root / "coverage/source-index.jsonl"
source.write_text(json.dumps({"bytes": 1, "kind": "source", "path": "linuxcnc/src/demo.c", "sha256": "different", "source_id": "build:demo"}) + "\n", encoding="utf-8")
report_path = root / "coverage/report.json"
generate(root, root / "out.jsonl", report_path)
report = json.loads(report_path.read_text(encoding="utf-8"))
self.assertTrue(report["hash_conflicts"])
self.assertFalse(report["missing_paths"])
def test_generation_is_deterministic(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
_, first = generate(root, root / "one.jsonl", root / "report.json")
_, second = generate(root, root / "two.jsonl", root / "report.json")
self.assertEqual(first, second)
self.assertEqual((root / "one.jsonl").read_bytes(), (root / "two.jsonl").read_bytes())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import subprocess
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class PosemathRuntimeTests(unittest.TestCase):
def test_native_posemath_binary(self):
binary = ROOT / "build-native/sim_posemath_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
result = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
def test_wasm_exports_posemath_api(self):
module = ROOT / "build-wasm/linuxcnc-sim-core.js"
if not module.is_file():
self.skipTest("WASM build has not been generated")
text = module.read_text(encoding="utf-8")
for symbol in ("sim_posemath_normalize", "sim_posemath_dot", "sim_posemath_line_point"):
self.assertIn(symbol, text)
def test_posemath_source_is_linked(self):
cmake = (ROOT / "wasm-core/CMakeLists.txt").read_text(encoding="utf-8")
self.assertIn("posemath.cc", cmake)
self.assertIn("target_link_libraries(sim_core PUBLIC sim_posemath)", cmake)
if __name__ == "__main__":
unittest.main()

68
scripts/test_protocol.py Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Smoke tests for the fixed protocol ABI and its generated WASM exports."""
from __future__ import annotations
import subprocess
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class ProtocolTests(unittest.TestCase):
def test_contract_declares_versions_and_errors(self):
header = (ROOT / "wasm-core/include/sim_protocol.h").read_text(encoding="utf-8")
for token in ("SIM_PROTOCOL_ERR_SCHEMA", "SIM_PROTOCOL_ERR_SEQUENCE", "sim_protocol_accept", "sim_protocol_last_seq"):
self.assertIn(token, header)
def test_native_protocol_binary_passes(self):
binary = ROOT / "build-native/sim_protocol_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
completed = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(completed.returncode, 0, completed.stderr.decode(errors="replace"))
def test_native_event_binary_passes(self):
binary = ROOT / "build-native/sim_event_test"
if not binary.is_file():
self.skipTest("native build has not been generated")
completed = subprocess.run([str(binary)], cwd=ROOT, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.assertEqual(completed.returncode, 0, completed.stderr.decode(errors="replace"))
def test_wasm_exports_protocol_functions(self):
module = ROOT / "build-wasm/linuxcnc-sim-core.js"
if not module.is_file():
self.skipTest("WASM build has not been generated")
text = module.read_text(encoding="utf-8")
for symbol in ("sim_protocol_reset", "sim_protocol_accept", "sim_protocol_last_seq", "sim_event_append", "sim_event_export"):
self.assertIn(symbol, text)
def test_worker_session_smoke(self):
completed = subprocess.run(
["node", "web/worker/test_sim_session.mjs"],
cwd=ROOT,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn("sim_session=passed", completed.stdout)
def test_real_wasm_session_smoke(self):
completed = subprocess.run(
["node", "web/worker/test_wasm_session.mjs"],
cwd=ROOT,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn("wasm_session=passed", completed.stdout)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_source_symbols import generate, parse_file
class SourceSymbolsTests(unittest.TestCase):
def test_parses_c_python_and_comp_entries(self):
with TemporaryDirectory() as directory:
root = Path(directory)
for sub in ("linuxcnc/src/emc", "linuxcnc/src/hal", "linuxcnc/lib/python"):
(root / sub).mkdir(parents=True, exist_ok=True)
c = root / "linuxcnc/src/emc/demo.c"
c.write_text("#define DEMO 1\nint demo(int x) { return x; }\n", encoding="utf-8")
py = root / "linuxcnc/lib/python/demo.py"
py.write_text("class Demo:\n async def run(self):\n pass\n", encoding="utf-8")
comp = root / "linuxcnc/src/hal/demo.comp"
comp.write_text("component demo;\npin in bit enable;\nfunction update;\n", encoding="utf-8")
rows, report = parse_file(root, c)
self.assertEqual({row["symbol"] for row in rows}, {"DEMO", "demo"})
self.assertEqual(report["parse_status"], "parsed")
rows, _ = parse_file(root, py)
self.assertEqual({row["symbol_kind"] for row in rows}, {"class", "async_function"})
rows, _ = parse_file(root, comp)
self.assertEqual({row["symbol_kind"] for row in rows}, {"component", "pin", "function"})
def test_generation_is_deterministic(self):
with TemporaryDirectory() as directory:
root = Path(directory)
for sub in ("linuxcnc/src/emc", "linuxcnc/src/hal", "linuxcnc/lib/python"):
(root / sub).mkdir(parents=True, exist_ok=True)
(root / "linuxcnc/src/emc/demo.c").write_text("void z(void) {}\n", encoding="utf-8")
first = root / "one.jsonl"
second = root / "two.jsonl"
report = root / "report.json"
_, digest_one = generate(root, first, report)
_, digest_two = generate(root, second, report)
self.assertEqual(digest_one, digest_two)
self.assertEqual(first.read_bytes(), second.read_bytes())
def test_decode_error_is_reported(self):
with TemporaryDirectory() as directory:
root = Path(directory)
path = root / "linuxcnc/src/emc"
path.mkdir(parents=True)
bad = path / "bad.c"
bad.write_bytes(b"\xff\xfe")
rows, report = parse_file(root, bad)
self.assertEqual(rows, [])
self.assertEqual(report["parse_status"], "decode_error")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_test_entries import generate
class TestEntriesTests(unittest.TestCase):
def create_tree(self):
directory = TemporaryDirectory()
root = Path(directory.name)
test_dir = root / "linuxcnc/tests/demo"
unit_dir = root / "linuxcnc/unit_tests/tp"
test_dir.mkdir(parents=True)
unit_dir.mkdir(parents=True)
(test_dir / "test.sh").write_text("#!/bin/sh\nlinuxcnc demo.ini\n", encoding="utf-8")
(test_dir / "demo.ini").write_text("[EMC]\nMACHINE = demo\n", encoding="utf-8")
(test_dir / "expected.txt").write_text("ok\n", encoding="utf-8")
(test_dir / "checkresult").write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
(unit_dir / "meson.build").write_text("test('blendmath', executable('test_blendmath', 'test_blendmath.c'))\n", encoding="utf-8")
(unit_dir / "test_blendmath.c").write_text("int main(void) { return 0; }\n", encoding="utf-8")
(root / "linuxcnc/meson.build").write_text("test('top', executable('test_top', 'x.c'))\n", encoding="utf-8")
return directory, root
def test_entries_contain_contract_fields(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
output = root / "coverage/test-entries.jsonl"
count, _ = generate(root, output, root / "coverage/report.json")
self.assertGreaterEqual(count, 4)
rows = [json.loads(line) for line in output.read_text(encoding="utf-8").splitlines()]
shell = next(row for row in rows if row["path"].endswith("/test.sh"))
self.assertEqual(shell["parse_status"], "parsed")
self.assertIn("linuxcnc/tests/demo/demo.ini", shell["inputs"])
self.assertIn("linuxcnc/tests/demo/expected.txt", shell["expected"])
self.assertEqual(shell["portability"], "portable_candidate")
def test_hardware_is_real_excluded(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
test_path = root / "linuxcnc/tests/demo/test.sh"
test_path.write_text("#!/bin/sh\nloadrt hostmot2\n", encoding="utf-8")
generate(root, root / "out.jsonl", root / "report.json")
rows = [json.loads(line) for line in (root / "out.jsonl").read_text(encoding="utf-8").splitlines()]
shell = next(row for row in rows if row["path"].endswith("/test.sh"))
self.assertEqual(shell["portability"], "REAL-EXCLUDED")
def test_generation_is_deterministic(self):
directory, root = self.create_tree()
self.addCleanup(directory.cleanup)
first, digest_one = root / "one.jsonl", None
_, digest_one = generate(root, first, root / "report.json")
second, digest_two = root / "two.jsonl", None
_, digest_two = generate(root, second, root / "report.json")
self.assertEqual(digest_one, digest_two)
self.assertEqual(first.read_bytes(), second.read_bytes())
if __name__ == "__main__":
unittest.main()

240
scripts/validate_baseline.py Executable file
View File

@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""Validate the M0 baseline and fail closed on unassigned coverage entries."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
REQUIRED_MANIFEST_FIELDS = {
"id", "source", "source_sha", "domain", "kind", "dependencies", "inputs",
"outputs", "errors", "sim_status", "web_status", "test_id", "oracle", "golden",
"difference", "coverage_dimensions", "context_id", "exclusion_reason",
}
ALLOWED_STATUS = {"planned", "in_progress", "implemented", "verified", "accepted", "blocked", "REAL-EXCLUDED"}
def read_jsonl(path: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
with path.open("r", encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
if not line.strip():
continue
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number}: expected object")
rows.append(value)
return rows
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def validate(repo_root: Path, lock_path: Path, source_path: Path, manifest_path: Path) -> list[str]:
errors: list[str] = []
lock = json.loads(lock_path.read_text(encoding="utf-8"))
for field in ("context_id", "linuxcnc_version", "linuxcnc_source_sha", "emscripten_version", "schema_versions"):
if not lock.get(field):
errors.append(f"context lock missing {field}")
sources = read_jsonl(source_path)
manifests = read_jsonl(manifest_path)
if lock.get("source_index_sha256") != sha256(source_path):
errors.append("source index hash does not match context lock")
if lock.get("manifest_seed_sha256") != sha256(manifest_path):
errors.append("manifest seed hash does not match context lock")
document_index_path = repo_root / "coverage/document-capabilities.jsonl"
document_index_hash = lock.get("document_capabilities_sha256")
if document_index_hash:
if not document_index_path.is_file():
errors.append("document capabilities index is missing")
elif document_index_hash != sha256(document_index_path):
errors.append("document capabilities index hash does not match context lock")
version_path = repo_root / "linuxcnc/VERSION"
if lock.get("linuxcnc_version") != version_path.read_text(encoding="utf-8").strip():
errors.append("linuxcnc version does not match context lock")
try:
source_sha = subprocess.check_output(
["git", "-C", str(repo_root / "linuxcnc"), "rev-parse", "HEAD"],
text=True,
).strip()
except (OSError, subprocess.CalledProcessError):
source_sha = None
if source_sha and lock.get("linuxcnc_source_sha") != source_sha:
errors.append("linuxcnc source commit does not match context lock")
inventory_count = lock.get("inventory_counts", {})
if isinstance(inventory_count, dict):
if inventory_count.get("total") != len(sources):
errors.append("source index count does not match context lock")
if inventory_count.get("source_index") != len(sources):
errors.append("source_index count does not match context lock")
if inventory_count.get("manifest_seed") != len(manifests):
errors.append("manifest_seed count does not match context lock")
if document_index_hash and inventory_count.get("document_capabilities") is not None:
if inventory_count.get("document_capabilities") != sum(
1 for line in document_index_path.read_text(encoding="utf-8").splitlines() if line.strip()
):
errors.append("document_capabilities count does not match context lock")
symbols_path = repo_root / "coverage/source-symbols.jsonl"
symbols_hash = lock.get("source_symbols_sha256")
if symbols_hash:
if not symbols_path.is_file():
errors.append("source symbols index is missing")
elif symbols_hash != sha256(symbols_path):
errors.append("source symbols index hash does not match context lock")
if isinstance(inventory_count, dict) and symbols_hash and inventory_count.get("source_symbols") is not None:
if inventory_count.get("source_symbols") != sum(1 for line in symbols_path.read_text(encoding="utf-8").splitlines() if line.strip()):
errors.append("source_symbols count does not match context lock")
resources_path = repo_root / "coverage/config-resources.jsonl"
resources_hash = lock.get("config_resources_sha256")
if resources_hash:
if not resources_path.is_file():
errors.append("config resources index is missing")
elif resources_hash != sha256(resources_path):
errors.append("config resources index hash does not match context lock")
if isinstance(inventory_count, dict) and resources_hash and inventory_count.get("config_resources") is not None:
if inventory_count.get("config_resources") != sum(1 for line in resources_path.read_text(encoding="utf-8").splitlines() if line.strip()):
errors.append("config_resources count does not match context lock")
test_entries_path = repo_root / "coverage/test-entries.jsonl"
test_entries_hash = lock.get("test_entries_sha256")
if test_entries_hash:
if not test_entries_path.is_file():
errors.append("test entries index is missing")
elif test_entries_hash != sha256(test_entries_path):
errors.append("test entries index hash does not match context lock")
if isinstance(inventory_count, dict) and test_entries_hash and inventory_count.get("test_entries") is not None:
if inventory_count.get("test_entries") != sum(1 for line in test_entries_path.read_text(encoding="utf-8").splitlines() if line.strip()):
errors.append("test_entries count does not match context lock")
normalized_path = repo_root / "coverage/normalized-sources.jsonl"
normalized_hash = lock.get("normalized_sources_sha256")
if normalized_hash:
if not normalized_path.is_file():
errors.append("normalized sources index is missing")
elif normalized_hash != sha256(normalized_path):
errors.append("normalized sources index hash does not match context lock")
if isinstance(inventory_count, dict) and normalized_hash and inventory_count.get("normalized_sources") is not None:
if inventory_count.get("normalized_sources") != sum(1 for line in normalized_path.read_text(encoding="utf-8").splitlines() if line.strip()):
errors.append("normalized_sources count does not match context lock")
closure_path = repo_root / "coverage/dependency-closure.jsonl"
closure_hash = lock.get("dependency_closure_sha256")
if closure_hash:
if not closure_path.is_file():
errors.append("dependency closure index is missing")
elif closure_hash != sha256(closure_path):
errors.append("dependency closure index hash does not match context lock")
if isinstance(inventory_count, dict) and closure_hash and inventory_count.get("dependency_edges") is not None:
if inventory_count.get("dependency_edges") != sum(1 for line in closure_path.read_text(encoding="utf-8").splitlines() if line.strip()):
errors.append("dependency_edges count does not match context lock")
golden_schema_path = repo_root / "coverage/golden-schema.json"
golden_schema_hash = lock.get("golden_schema_sha256")
if golden_schema_hash:
if not golden_schema_path.is_file():
errors.append("golden schema is missing")
elif golden_schema_hash != sha256(golden_schema_path):
errors.append("golden schema hash does not match context lock")
runner_report_path = repo_root / "coverage/native-runner.report.json"
runner_report_hash = lock.get("native_runner_report_sha256")
if runner_report_hash:
if not runner_report_path.is_file():
errors.append("native runner report is missing")
elif runner_report_hash != sha256(runner_report_path):
errors.append("native runner report hash does not match context lock")
recovery_report_path = repo_root / "coverage/workspace-recovery.report.json"
recovery_report_hash = lock.get("workspace_recovery_report_sha256")
if recovery_report_hash:
if not recovery_report_path.is_file():
errors.append("workspace recovery report is missing")
elif recovery_report_hash != sha256(recovery_report_path):
errors.append("workspace recovery report hash does not match context lock")
runtime_report_path = repo_root / "coverage/runtime-mapping.report.json"
runtime_report_hash = lock.get("runtime_mapping_report_sha256")
if runtime_report_hash:
if not runtime_report_path.is_file():
errors.append("runtime mapping report is missing")
elif runtime_report_hash != sha256(runtime_report_path):
errors.append("runtime mapping report hash does not match context lock")
python_manifest_path = repo_root / "python-runtime/manifest.json"
python_manifest_hash = lock.get("python_runtime_manifest_sha256")
if python_manifest_hash:
if not python_manifest_path.is_file():
errors.append("python runtime manifest is missing")
elif python_manifest_hash != sha256(python_manifest_path):
errors.append("python runtime manifest hash does not match context lock")
python_report_path = repo_root / "coverage/python-runtime.report.json"
python_report_hash = lock.get("python_runtime_report_sha256")
if python_report_hash:
if not python_report_path.is_file():
errors.append("python runtime report is missing")
elif python_report_hash != sha256(python_report_path):
errors.append("python runtime report hash does not match context lock")
hal_report_path = repo_root / "coverage/hal-runtime.report.json"
hal_report_hash = lock.get("hal_runtime_report_sha256")
if hal_report_hash:
if not hal_report_path.is_file():
errors.append("hal runtime report is missing")
elif hal_report_hash != sha256(hal_report_path):
errors.append("hal runtime report hash does not match context lock")
posemath_report_path = repo_root / "coverage/posemath-runtime.report.json"
posemath_report_hash = lock.get("posemath_runtime_report_sha256")
if posemath_report_hash:
if not posemath_report_path.is_file():
errors.append("posemath runtime report is missing")
elif posemath_report_hash != sha256(posemath_report_path):
errors.append("posemath runtime report hash does not match context lock")
source_ids = [str(row.get("source_id", "")) for row in sources]
if len(source_ids) != len(set(source_ids)):
errors.append("source index contains duplicate source_id")
manifest_ids = [str(row.get("id", "")) for row in manifests]
if len(manifest_ids) != len(set(manifest_ids)):
errors.append("manifest contains duplicate id")
source_sha_by_id = {str(row["source_id"]): str(row["sha256"]) for row in sources}
manifest_sources = {str(row.get("source", "")) for row in manifests}
missing = sorted(set(source_ids) - manifest_sources)
if missing:
errors.append(f"manifest has {len(missing)} orphan sources; first={missing[0]}")
extra = sorted(manifest_sources - set(source_ids))
if extra:
errors.append(f"manifest has {len(extra)} unknown sources; first={extra[0]}")
for row in manifests:
missing_fields = REQUIRED_MANIFEST_FIELDS - row.keys()
if missing_fields:
errors.append(f"{row.get('id')}: missing fields {sorted(missing_fields)}")
source_id = str(row.get("source", ""))
if source_id in source_sha_by_id and row.get("source_sha") != source_sha_by_id[source_id]:
errors.append(f"{row.get('id')}: source_sha does not match source index")
for field in ("sim_status", "web_status"):
if row.get(field) not in ALLOWED_STATUS:
errors.append(f"{row.get('id')}: invalid {field}={row.get(field)!r}")
if row.get("sim_status") == "accepted" or row.get("web_status") == "accepted":
if not row.get("feature_id") or not row.get("test_id") or not row.get("oracle") or not row.get("context_id"):
errors.append(f"{row.get('id')}: accepted entry lacks feature/test/oracle/context evidence")
return errors
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--lock", type=Path, default=Path("context-lock.json"))
parser.add_argument("--source-index", type=Path, default=Path("coverage/source-index.jsonl"))
parser.add_argument("--manifest", type=Path, default=Path("coverage/manifest.jsonl"))
args = parser.parse_args()
root = args.repo_root.resolve()
errors = validate(root, root / args.lock, root / args.source_index, root / args.manifest)
if errors:
for error in errors:
print(f"ERROR: {error}")
return 1
print("baseline=valid")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Validate external artifact provenance before it enters a build context."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from urllib.parse import urlparse
SHA256 = re.compile(r"^[0-9a-f]{64}$")
KINDS = {"official", "china-mirror", "local-cache"}
def validate(path: Path) -> list[str]:
errors: list[str] = []
seen: set[tuple[str, str]] = set()
with path.open("r", encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
if not line.strip():
continue
try:
row = json.loads(line)
except json.JSONDecodeError as exc:
errors.append(f"{path}:{line_number}: invalid JSON: {exc.msg}")
continue
if row.get("record_type") != "artifact":
continue
required = {"name", "version", "source_url", "sha256", "bytes", "license", "mirror_kind"}
missing = sorted(required - row.keys())
if missing:
errors.append(f"{path}:{line_number}: missing {missing}")
continue
key = (str(row["name"]), str(row["version"]))
if key in seen:
errors.append(f"{path}:{line_number}: duplicate artifact {key[0]}@{key[1]}")
seen.add(key)
if not SHA256.fullmatch(str(row["sha256"])):
errors.append(f"{path}:{line_number}: sha256 must be 64 lowercase hex characters")
if not isinstance(row["bytes"], int) or row["bytes"] <= 0:
errors.append(f"{path}:{line_number}: bytes must be a positive integer")
if row["mirror_kind"] not in KINDS:
errors.append(f"{path}:{line_number}: invalid mirror_kind")
parsed = urlparse(str(row["source_url"]))
if row["mirror_kind"] != "local-cache" and parsed.scheme not in {"http", "https"}:
errors.append(f"{path}:{line_number}: source_url must be HTTP(S) for downloaded artifacts")
if row["mirror_kind"] == "china-mirror" and not row.get("origin_url"):
errors.append(f"{path}:{line_number}: china-mirror requires origin_url")
return errors
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--index", type=Path, default=Path("vendor/downloads/index.jsonl"))
args = parser.parse_args()
errors = validate(args.index)
if errors:
for error in errors:
print(f"ERROR: {error}")
return 1
print(f"download_index=valid path={args.index}")
return 0
if __name__ == "__main__":
raise SystemExit(main())