From 11fc476ce2e44aee6a1bd8bce9dc4282f502b12e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 14 Oct 2026 23:14:56 -0400 Subject: [PATCH 1/6] fix(nz): replace drifting spec line citations with stable locators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invariant: No resource in the NZ spec package (packages/microcosm-build/src/microcosm/build/nz/, JSON and spec/*.yaml) cites a line number of a file in this repository. Another repository's file may be cited by line only when the same resource pins that file at a commit, in a mapping whose path names the file and whose commit is 7–40 lowercase hex. Rebuild #1141 on current main without merging its branch. Replace all local Python/JSON/Markdown line locators with symbols, exact JSON records, or a documentation heading. Preserve resource meanings and field shapes. Guard all extensions, nested values/keys, foreign pins, symbol resolution and JSON pointers; add properties and differentials. Regenerate only changed NZ golden resource hashes and fingerprint: benefit_unit_rule.json: 5c1cf16daaae482ea3ec53495f4c2e4ae452d928c6240a5284dbbf30cd2dfe37 -> a9e8cbfd38a64d2f8db0002fe8c31bb5a18fb34dbe59fa943336487ce95e2ca8 reporting_classifiers.json: b5af9e4737af4717d86aedfe651df8b3c18c3875763726e7a6a0b9b1168ed133 -> 789a07acd1b6724a12bbd9584ab9fa05bb01d0b675cc3a0fbc837b066dbec49e scenarios.json: 30f96f3063773d77e3c70119c8f5d99d3528eca87428903da00ca071ab34cfaf -> 9c70241a6203b3a66dc09587c8b955d9d421bbe32f018ea1542e7639f4c7507c country_fingerprint: 3208901a27e6f09885e5990517c80e2d418425d937aa33fb0f6dacbbf290dd23 -> 5e97ffb44bc37ebd3809e7f6cf2ad253ef4308339338b96b4bd78746a9c1888d package_fingerprint: 07ab080d68771a207290c83a9040b294f0775c96b74022938c9da13aa713ba20 -> bba0d1ac41b972314bfa5f95adb9884c462296713af50f68d3969246a0c6f112 No other repository pins moved; searched full hashes and 8-char prefixes. package_fingerprint is derived; no file pins it. spec_sha256 and documentation_sha256 are unchanged. Validation: 542 passed, 1 skipped across eight final targeted test-file runs; all four required mutants fail their intended tests and are restored. Ruff check/format and git diff --check pass. Co-Authored-By: GPT-6.1 Sol --- changelog.d/nz-spec-line-citations.fixed.md | 3 + .../microcosm/build/nz/benefit_unit_rule.json | 8 +- .../build/nz/reporting_classifiers.json | 10 +- .../src/microcosm/build/nz/scenarios.json | 2 +- .../shared/test_nz_spec_package.py | 907 +++++++++++++++++- .../tests/golden/nz_country_spec.json | 8 +- 6 files changed, 921 insertions(+), 17 deletions(-) create mode 100644 changelog.d/nz-spec-line-citations.fixed.md diff --git a/changelog.d/nz-spec-line-citations.fixed.md b/changelog.d/nz-spec-line-citations.fixed.md new file mode 100644 index 0000000000..3dfc17f318 --- /dev/null +++ b/changelog.d/nz-spec-line-citations.fixed.md @@ -0,0 +1,3 @@ +Replace drifting New Zealand spec citations with Python symbols, JSON Pointers, +and a documentation heading. Guard every NZ JSON/YAML resource against local +file line citations and require commit pins for foreign file line citations. diff --git a/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json b/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json index 9941ed4917..bf3be694eb 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json +++ b/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json @@ -2,7 +2,7 @@ "version": 1, "country": "nz", "method_card_row": "MC7", - "description": "The New Zealand benefit unit (engine Family entity): an adult, their co-resident partner, and their dependent children; every other adult forms their own unit. An adult for unit construction is any person excluded from the declared dependent-child rule, including a financially independent young person or an independent teen parent. The unit id family.family_id and membership person.person_family_id are structural graph columns (microcosm.graph.executor._structural_columns, executor.py:638-645); a non-structural node cannot own either (executor.py:1549-1554). family.family_household_id is the ordinary family column linking each unit to its household. The shared transport CREATE kernel (G4/G5a; transport/population_kernels.py:208-232) builds units from donor concept pointers under this spec. Family weights are inherited from the household through the Frame.", + "description": "The New Zealand benefit unit (engine Family entity): an adult, their co-resident partner, and their dependent children; every other adult forms their own unit. An adult for unit construction is any person excluded from the declared dependent-child rule, including a financially independent young person or an independent teen parent. The unit id family.family_id and membership person.person_family_id are structural graph columns (microcosm.graph.executor._structural_columns); a non-structural node cannot own either (microcosm.graph.executor._validate_population_declaration). family.family_household_id is the ordinary family column linking each unit to its household. The shared transport CREATE kernel (G4/G5a; microcosm.build.transport.population_kernels.TransportCreateKernel) builds units from donor concept pointers under this spec. Family weights are inherited from the household through the Frame.", "status": "settled by the NZ hub on 2026-10-12 under G8 method addendum B8 (MC7)", "entity": "family", "engine_entity": "Family", @@ -19,7 +19,7 @@ "parent_2_person_id" ], "household_reference_person": "reference_person_id", - "citation": "packages/microcosm-frame/src/microcosm/frame/concepts.py:889-928" + "citation": "packages/microcosm-frame/src/microcosm/frame/concepts.py, the \"# --- Relationships\" block: _pointer(\"partner_person_id\") through _pointer(\"reference_person_id\")" }, "source_household_lineage": { "concept": "fact:household.source_household_id", @@ -37,7 +37,7 @@ }, { "member": "dependent_child", - "rule": "A dependent child joins the unit of its co-resident parent parent_1_person_id; if that parent is itself dependent, follow the parent chain to that parent's unit. A lone co-resident parent is always parent 1 (concepts.py:900-907), so parent_2_person_id never chooses the unit." + "rule": "A dependent child joins the unit of its co-resident parent parent_1_person_id; if that parent is itself dependent, follow the parent chain to that parent's unit. A lone co-resident parent is always parent 1 (concepts.py _pointer(\"parent_1_person_id\")), so parent_2_person_id never chooses the unit." }, { "member": "other_adult", @@ -55,7 +55,7 @@ "hours_concept": "fact:person.usual_weekly_hours", "donor_variable": "weekly_hours_worked_before_lsr", "basis": "Social Security Act 2018, Schedule 2, financially independent (DLM6784504, limb a) and full employment (DLM6784514, limbs a and b), as at 1 April 2026; G8 method addendum B8. The test applies only while below age_limit_years.", - "concept_evidence": "concepts.py:1069-1083 declares usual weekly hours across all jobs and self-employment in worked weeks in the donor reference year, with transport CARRY. adapters/policyengine_us_concepts.py:352-362 maps weekly_hours_worked_before_lsr through Identity to this concept; transport.py:843-849 decodes the donor through that mapping. docs/concept-coverage/policyengine-us.md:31 records the same binding.", + "concept_evidence": "concepts.py concept(\"fact:person.usual_weekly_hours\") declares usual weekly hours across all jobs and self-employment in worked weeks in the donor reference year, with transport CARRY. microcosm.frame.adapters.policyengine_us_concepts.POLICYENGINE_US_CONCEPT_MAPPING maps weekly_hours_worked_before_lsr through Identity to this concept; microcosm.frame.transport.read_populace_us_donor decodes the donor through that mapping. docs/concept-coverage/policyengine-us.md#inputs-each-concept-feeds records the same binding.", "approximation": "The donor usual rate in worked weeks is used for the statutory average weekly employment test; employment contracts and the averaging interval are not observed." }, "parent_independence_min_age": 16, diff --git a/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json b/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json index 9ca1423e8c..392adcf03f 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json @@ -192,8 +192,8 @@ "stability": "Same pinned donor and unit rule preserve person ids across rebuilds; each family id is its head person id. Units sharing source household lineage remain distinct units.", "implementation": [ "packages/microcosm-frame/src/microcosm/frame/unit_construction.py build_benefit_units (each family id is its head person's id)", - "packages/microcosm-build/src/microcosm/build/transport/population_kernels.py:209-228", - "packages/microcosm-frame/src/microcosm/frame/transport.py:843-856" + "microcosm.build.transport.population_kernels.TransportCreateKernel", + "microcosm.frame.transport.read_populace_us_donor" ], "citation": { "item": "A1", @@ -260,7 +260,7 @@ "disclosures": [ { "note": "NZ Super is overstated for single people who share a home because the shares-residence input defaults false; this moves their deciles upward.", - "evidence": "axiom_input_closure.json:1045-1052", + "evidence": "axiom_input_closure.json#/entries/99", "citation": { "item": "A1", "source": "docs/nz-reporting-classifiers-method.md#a1-income-decile-provisional" @@ -268,7 +268,7 @@ }, { "note": "Market-income components follow the existing family gross-income concept mapping. Rental income is after business expenses. The rental and private-pension concept descriptions do not explicitly say before tax; their before-tax transport interpretation must be verified before activation.", - "evidence": "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json:670-688; packages/microcosm-frame/src/microcosm/frame/concepts.py:990-1000,1040-1051", + "evidence": "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json#/bindings/40; packages/microcosm-frame/src/microcosm/frame/concepts.py _amount(\"rental_income\") and _amount(\"private_pension_income\")", "citation": { "item": "A1", "source": "docs/nz-reporting-classifiers-method.md#a1-income-decile-provisional" @@ -734,7 +734,7 @@ "as_non_beneficiary_audit": { "nz_super": "The existing accommodation_supplement_non_beneficiary state binding negates any_true over assigned member receipt flags, including NZ Super. A unit with any assigned NZ Super receipt is therefore not a non-beneficiary.", "veterans_pension": "The same binding omits the unsupported Veterans Pension flag, so it must be extended when that receipt input is activated. A missing Veterans Pension flag continues to block reporting rather than classify its recipients as other.", - "evidence": "axiom_input_closure.json:1183-1204", + "evidence": "axiom_input_closure.json#/entries/115", "citation": { "item": "A2", "source": "docs/nz-reporting-classifiers-method.md#a2-receipt-group" diff --git a/packages/microcosm-build/src/microcosm/build/nz/scenarios.json b/packages/microcosm-build/src/microcosm/build/nz/scenarios.json index 6fba457c8f..8a819e9923 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/scenarios.json +++ b/packages/microcosm-build/src/microcosm/build/nz/scenarios.json @@ -150,7 +150,7 @@ "annualisation": { "weeks_per_year": 52, "method_card_row": "MC12", - "note": "the conformance harness uses 365/7 weeks (ops main run.py:168); the model states and uses 52" + "note": "the conformance harness uses 365/7 weeks (WEEKS_IN_MODEL_YEAR in TheAxiomFoundation/ops nz-lane/emtr_reproduction/run.py at ba6e7d6e); the model states and uses 52" }, "groupings": { "method_card_row": "MC3", diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index bab2dc5346..e287568b51 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -19,12 +19,15 @@ (``modal_area``); - every scenario names exactly one method-card row and a tier from {entitlement, calibration}, and changes exactly one knob from the centre - (``scenario_errors``). + (``scenario_errors``); +- no resource cites a line of a repository file, and foreign-file line + citations require a same-resource commit pin (``unpinned_line_citations``). Differential checks compare two records of one fact: the loader's target references with a direct parse, the rules bindings' module digests with the committed Axiom input surface, and the export contract's formula-owned list -with the bound variables. +with the bound variables, and source citations with the code and JSON nodes +they identify. """ from __future__ import annotations @@ -32,10 +35,16 @@ import copy import dataclasses import hashlib +import importlib +import inspect import json import math import re +import subprocess from collections.abc import Callable, Iterator, Mapping, Sequence +from functools import cache +from pathlib import PurePosixPath +from types import SimpleNamespace from typing import Any import pytest @@ -49,6 +58,8 @@ reference_fact_selectors, ) from microcosm.build.spec_engine import load_yaml12 +from microcosm.frame import concepts as frame_concepts +from microcosm.frame.input_closure import INPUT_CLOSURE_FORMAT from test_support.paths import paths_for _BUILD = paths_for("microcosm-build") @@ -97,10 +108,27 @@ def _nested_keys(value: object) -> Iterator[str]: yield from _nested_keys(child) +def _nested_strings(value: object) -> Iterator[str]: + """Every string in a payload, mapping keys included, at any depth.""" + + if isinstance(value, Mapping): + for key, child in value.items(): + yield str(key) + yield from _nested_strings(child) + elif isinstance(value, list): + for child in value: + yield from _nested_strings(child) + elif isinstance(value, str): + yield value + + def _package_payloads() -> dict[str, object]: payloads: dict[str, object] = {} for path in sorted(NZ_ROOT.rglob("*")): - if not path.is_file(): + if not path.is_file() or not ( + path.suffix == ".json" + or (path.parent == NZ_ROOT / "spec" and path.suffix == ".yaml") + ): continue name = path.relative_to(NZ_ROOT).as_posix() text = path.read_text(encoding="utf-8") @@ -459,6 +487,218 @@ def scenario_errors(document: Mapping[str, Any]) -> list[str]: return errors +#: A file citation, with any extension, optionally qualified by directories. +_SOURCE_FILE = ( + r"(?P[A-Za-z0-9_./-]*[A-Za-z0-9_-]\.[A-Za-z0-9][A-Za-z0-9_-]*)" + r"(?![A-Za-z0-9_.-])" +) +#: Keep every locator spelling from #1141, now for any file extension. +_LINE_CITATIONS = ( + re.compile(_SOURCE_FILE + r"(?:\s*:\s*|#L|\s+L|,?\s*\(?\s*[Ll]ines?\s+)\d"), + re.compile( + r"\b[Ll]ines?\s+\d+(?:\s*[-–]\s*\d+)?(?:\s*,\s*\d+)*\s+(?:of|in)\s+" + + _SOURCE_FILE + ), +) +_COMMIT_PIN = re.compile(r"[0-9a-f]{7,40}") + + +def source_line_citations(value: object) -> list[tuple[str, str]]: + """Each (file path, string) citing a line, including nested mapping keys.""" + + return [ + (str(PurePosixPath(match["file"])), text) + for text in _nested_strings(value) + for pattern in (*_LINE_CITATIONS, *_extensionless_line_patterns()) + for match in pattern.finditer(text) + ] + + +@cache +def _repository_files() -> frozenset[str]: + """Tracked and newly added checkout files, without traversing environments.""" + + result = subprocess.run( + ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], + cwd=_BUILD.repository, + check=True, + capture_output=True, + ) + return frozenset(result.stdout.decode().split("\0")) - {""} + + +@cache +def _extensionless_line_patterns() -> tuple[re.Pattern[str], ...]: + # Bare words in prose are not file names. Recognize extensionless names + # only when the checkout actually contains such a file (also .gitignore). + names = { + PurePosixPath(path).name + for path in _repository_files() + if not PurePosixPath(path).suffix + } + alternatives = "|".join(re.escape(name) for name in sorted(names)) + source = rf"(?P[A-Za-z0-9_./-]*(?:{alternatives}))(?![A-Za-z0-9_.-])" + return ( + re.compile(source + r"(?:\s*:\s*|#L|\s+L|,?\s*\(?\s*[Ll]ines?\s+)\d"), + re.compile( + r"\b[Ll]ines?\s+\d+(?:\s*[-–]\s*\d+)?(?:\s*,\s*\d+)*\s+(?:of|in)\s+" + + source + ), + ) + + +def _repository_source(path: str) -> bool: + root = _BUILD.repository.resolve() + candidate = (root / path).resolve() + if ( + candidate.is_relative_to(root) + and candidate.relative_to(root).as_posix() in _repository_files() + ): + return True + name = str(PurePosixPath(path)) + return any( + tracked == name or tracked.endswith("/" + name) + for tracked in _repository_files() + ) + + +def commit_pinned_sources(value: object) -> set[str]: + """Paths named by same-mapping lowercase 7–40-hex commit pins.""" + + pinned: set[str] = set() + if isinstance(value, Mapping): + path, commit = value.get("path"), value.get("commit") + if ( + isinstance(path, str) + and re.fullmatch(r"[A-Za-z0-9_./-]+", path) + and PurePosixPath(path).name not in {"", ".", ".."} + and isinstance(commit, str) + and _COMMIT_PIN.fullmatch(commit) + ): + pinned.add(str(PurePosixPath(path))) + for child in value.values(): + pinned |= commit_pinned_sources(child) + elif isinstance(value, list): + for child in value: + pinned |= commit_pinned_sources(child) + return pinned + + +def _pin_covers(cited: str, pinned: str) -> bool: + # The existing ops resource cites bare run.py but pins its complete path. + # Qualified citations cannot borrow a pin from another directory's file. + return cited == pinned or ("/" not in cited and PurePosixPath(pinned).name == cited) + + +def unpinned_line_citations(document: object) -> list[tuple[str, str]]: + """Refuse all local-file lines and foreign-file lines without a file pin.""" + + pinned = commit_pinned_sources(document) + return [ + (name, text) + for name, text in source_line_citations(document) + if _repository_source(name) or not any(_pin_covers(name, pin) for pin in pinned) + ] + + +# Retain #1141's public guard spelling while widening its scope to all files. +unpinned_python_line_citations = unpinned_line_citations + + +_DOTTED_MICROCOSM_SYMBOL = re.compile( + r"(?(?:/[^\s,;()\"']*)+)") + + +def resolve_dotted_symbol(dotted: str) -> object: + """Import the longest module prefix, then resolve its attributes.""" + + parts = dotted.split(".") + for cut in range(len(parts), 0, -1): + name = ".".join(parts[:cut]) + try: + target: object = importlib.import_module(name) + except ModuleNotFoundError as error: + if error.name is None or not ( + name == error.name or name.startswith(error.name + ".") + ): + raise + continue + if cut == 1: + raise ModuleNotFoundError(dotted, name=dotted) + for attribute in parts[cut:]: + target = getattr(target, attribute) + return target + raise ModuleNotFoundError(dotted, name=dotted) + + +def dotted_symbol_citations(value: object) -> set[str]: + """Find Python symbols, distinguishing the existing serialized format ID.""" + + if isinstance(value, Mapping): + return { + dotted + for key, child in value.items() + for nested in ( + [str(key)] + if key == "format" and child == INPUT_CLOSURE_FORMAT + else [str(key), child] + ) + for dotted in dotted_symbol_citations(nested) + } + if isinstance(value, list): + return {dotted for child in value for dotted in dotted_symbol_citations(child)} + if isinstance(value, str): + return set(_DOTTED_MICROCOSM_SYMBOL.findall(value)) + return set() + + +def json_pointer_citations(value: object) -> list[tuple[str, str]]: + return [ + (match["file"], match["pointer"]) + for text in _nested_strings(value) + for match in _JSON_POINTER_CITATION.finditer(text) + ] + + +def resolve_json_pointer(file: str, pointer: str) -> object: + """Resolve an RFC 6901 pointer in one existing checkout JSON resource.""" + + relative = str(PurePosixPath(file)) + candidates = { + (_BUILD.repository / candidate).resolve() + for candidate in _repository_files() + if candidate == relative or candidate.endswith("/" + relative) + } + if len(candidates) != 1: + raise ValueError(f"JSON citation needs one checkout file: {file}: {candidates}") + (path,) = candidates + if not path.is_relative_to(_BUILD.repository.resolve()) or not path.is_file(): + raise ValueError(f"JSON citation leaves checkout or names no file: {file}") + if pointer and not pointer.startswith("/"): + raise ValueError(f"Invalid JSON pointer: {pointer}") + content = path.read_text(encoding="utf-8") + selected: object = ( + load_yaml12(content, source=relative) + if path.suffix in {".yaml", ".yml"} + else json.loads(content) + ) + for token in pointer.split("/")[1:]: + if re.search(r"~(?![01])", token): + raise ValueError(f"Invalid JSON pointer escape: {pointer}") + key = token.replace("~1", "/").replace("~0", "~") + if isinstance(selected, list): + if not re.fullmatch(r"0|[1-9][0-9]*", key): + raise ValueError(f"Invalid JSON array index: {key}") + selected = selected[int(key)] + elif isinstance(selected, Mapping): + selected = selected[key] + else: + raise TypeError(f"JSON pointer traverses a scalar: {file}#{pointer}") + return selected + + # --------------------------------------------------------------------------- # Reference sets # --------------------------------------------------------------------------- @@ -1581,6 +1821,667 @@ def test_every_refusal_branch_names_the_broken_scenario( assert expected in scenario_errors(document) +# --------------------------------------------------------------------------- +# Source citations +# --------------------------------------------------------------------------- + + +_SOURCE_STEM = st.from_regex(r"[a-z_][a-z0-9_]{0,15}", fullmatch=True) +_SOURCE_EXTENSION = st.from_regex(r"[A-Za-z0-9][A-Za-z0-9_]{0,7}", fullmatch=True) +_SOURCE_DIRECTORY = st.sampled_from( + ( + "", + "packages/microcosm-frame/src/microcosm/frame/", + "microcosm/graph/", + "nz-lane/emtr_reproduction/", + ) +) +#: Line-locator spellings: {file} is the cited source, {a} and {b} lines. +_LINE_LOCATORS = ( + "{file}:{a}", + "{file}:{a}-{b}", + "{file}:{a},{b}", + "{file}#L{a}", + "{file}#L{a}-L{b}", + "{file} L{a}", + "{file} line {a}", + "{file} lines {a}-{b}", + "{file}, lines {a}-{b}", + "{file} (lines {a}-{b})", + "Lines {a}-{b} of {file}", + "line {a} in {file}", +) +#: Symbol-citation spellings, which carry no line number. +_SYMBOL_LOCATORS = ( + "{file} {symbol}", + '{file} _pointer("{symbol}")', + '{file}, the "# --- Relationships" block', + "{file}: {symbol}", + "{symbol} in {file} at main", +) +#: Prose around a citation. It has no digit, so it can neither extend a +#: planted line number nor plant a citation of its own. +_PROSE = st.text( + alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ,;()'\"-_", + max_size=40, +) +_BRANCH_NAMES = ( + "main", + "HEAD", + "launch/ga-path-execution-gate", + "ba6e7d", + "BA6E7D6", + "", +) + + +def _plant(text: str, depth: int, as_key: bool) -> object: + planted: object = {text: "cited"} if as_key else text + for level in range(depth): + planted = {"level": level, "note": [planted]} + return planted + + +class TestSourceCitations: + def test_local_file_pins_cannot_hide_absolute_or_parent_path_aliases(self) -> None: + relative = "packages/microcosm-frame/src/microcosm/frame/concepts.py" + aliases = [ + str(_BUILD.repository / relative), + f"../{_BUILD.repository.name}/{relative}", + "packages/microcosm-frame/src/microcosm/frame/../frame/concepts.py", + ] + for source in aliases: + citation = f"{source}:889-928" + assert unpinned_line_citations( + { + "source": {"path": source, "commit": "abcdef0"}, + "note": citation, + } + ) == [(source, citation)] + + @pytest.mark.parametrize("source", ["LICENSE", ".gitignore", "Dockerfile"]) + def test_extensionless_repository_files_are_also_refused(self, source: str) -> None: + citation = f"{source}:1-3" + assert unpinned_line_citations( + { + "source": {"path": source, "commit": "abcdef0"}, + "note": citation, + } + ) == [(source, citation)] + + def test_the_input_closure_format_identifier_is_not_a_python_symbol(self) -> None: + assert dotted_symbol_citations({"format": INPUT_CLOSURE_FORMAT}) == set() + assert dotted_symbol_citations({"note": INPUT_CLOSURE_FORMAT}) == { + INPUT_CLOSURE_FORMAT + } + + def test_nested_dotted_attributes_resolve_after_the_module_prefix(self) -> None: + assert ( + resolve_dotted_symbol("microcosm.frame.concepts.TransportRule.CARRY") + is frame_concepts.TransportRule.CARRY + ) + + def test_json_pointer_keys_use_rfc6901_slash_escaping(self) -> None: + selected = resolve_json_pointer( + "reporting_classifiers.json", + "/parameter_provenance/~1income_decile~1entity", + ) + assert selected["item"] == "A1" + + def test_create_and_donor_reader_citations_still_describe_unit_inputs(self) -> None: + from microcosm.build.transport.population_kernels import TransportCreateKernel + from microcosm.frame.transport import read_populace_us_donor + from microcosm.graph.decl import StructuralDelta + + rule = _load("benefit_unit_rule.json") + reporting = _load("reporting_classifiers.json") + for document in [rule, reporting["tie_key_evidence"]]: + named = dotted_symbol_citations(document) + assert ( + "microcosm.build.transport.population_kernels.TransportCreateKernel" + in named + ) + assert ( + "microcosm.frame.transport.read_populace_us_donor" + in dotted_symbol_citations(reporting) + ) + assert TransportCreateKernel.capabilities.structural is StructuralDelta.CREATE + # Differential source evidence: CREATE reads the verified concept donor, + # then builds family units using the declared benefit-unit rule. + create_source = inspect.getsource(TransportCreateKernel) + assert create_source.index("read_populace_us_donor(") < create_source.index( + "build_benefit_units(" + ) + assert "tables, dropped = split_for_transport(donor.tables)" in create_source + assert "BenefitUnitRule.from_spec(" in create_source + assert 'tables["person"], tables["household"], rule' in create_source + donor_source = inspect.getsource(read_populace_us_donor) + assert "tables = POLICYENGINE_US_CONCEPT_MAPPING.decode(raw)" in donor_source + assert 'tables["person"][name] = values' in donor_source + assert 'tables["household"]["reference_person_id"]' in donor_source + assert 'tables["household"][SOURCE_HOUSEHOLD_ID_COLUMN]' in donor_source + + def test_financial_independence_citations_still_bind_carried_usual_hours( + self, + ) -> None: + from microcosm.frame.adapters.policyengine_us_concepts import ( + POLICYENGINE_US_CONCEPT_MAPPING, + ) + from microcosm.frame.concept_mapping import Identity + + rule = _load("benefit_unit_rule.json")["dependent_child"][ + "financial_independence" + ] + hours = frame_concepts.concept(rule["hours_concept"]) + assert hours.id == "fact:person.usual_weekly_hours" + assert "across all jobs and self-employment" in hours.definition + assert "in the weeks the person worked" in hours.definition + assert "reference year" in hours.definition + assert hours.transport is frame_concepts.TransportRule.CARRY + (binding,) = [ + binding + for binding in POLICYENGINE_US_CONCEPT_MAPPING.bindings + if binding.engine_input == rule["donor_variable"] + ] + assert binding.engine_entity == "person" + assert binding.concepts == (hours.id,) + assert isinstance(binding.transform, Identity) + assert binding.relation is frame_concepts.AlignmentRelation.EXACT + evidence = rule["concept_evidence"] + assert 'concept("fact:person.usual_weekly_hours")' in evidence + assert ( + "docs/concept-coverage/policyengine-us.md#inputs-each-concept-feeds" + in evidence + ) + assert ( + "microcosm.frame.adapters.policyengine_us_concepts.POLICYENGINE_US_CONCEPT_MAPPING" + in evidence + ) + assert "microcosm.frame.transport.read_populace_us_donor" in evidence + coverage = _BUILD.repository / "docs/concept-coverage/policyengine-us.md" + source = coverage.read_text(encoding="utf-8") + assert "## Inputs each concept feeds" in source + (row,) = [ + row for row in source.splitlines() if row.startswith(f"| `{hours.id}` |") + ] + assert f"`{rule['donor_variable']}`" in row + + def test_reporting_disclosure_cites_the_sharing_home_default_record(self) -> None: + disclosure = _load("reporting_classifiers.json")["disclosures"][0] + (citation,) = json_pointer_citations(disclosure["evidence"]) + record = resolve_json_pointer(*citation) + assert ( + record["input"] + == "nz_super_shares_residence_with_person_at_least_adult_age" + ) + assert record["class"] == "defaulted" + assert record["value"] is False + assert "overstates NZS for singles who share a home" in record["reason"] + assert "overstated for single people who share a home" in disclosure["note"] + + def test_reporting_non_beneficiary_audit_cites_its_exact_state_binding( + self, + ) -> None: + document = _load("reporting_classifiers.json") + (citation,) = json_pointer_citations( + document["as_non_beneficiary_audit"]["evidence"] + ) + record = resolve_json_pointer(*citation) + assert record["input"] == "accommodation_supplement_non_beneficiary" + assert record["class"] == "encoded" + binding = record["state_binding"] + assert binding["test"] == "any_true" + assert binding["group_rule"] == "any_member" + assert binding["negate"] is True + assert binding["columns"] == [ + "person.receives_jobseeker_support", + "person.receives_sole_parent_support", + "person.receives_supported_living_payment", + "person.receives_nz_super", + ] + assert "person.receives_veterans_pension" not in binding["columns"] + assert ( + "must be extended" + in document["as_non_beneficiary_audit"]["veterans_pension"] + ) + + def test_reporting_market_income_disclosure_cites_the_mapping_it_describes( + self, + ) -> None: + document = _load("reporting_classifiers.json") + disclosure = document["disclosures"][1] + (citation,) = json_pointer_citations(disclosure["evidence"]) + binding = resolve_json_pointer(*citation) + assert ( + binding["engine_input"] == "accommodation_supplement_relevant_weekly_income" + ) + assert binding["engine_entity"] == "Family" + assert binding["group_rule"] == "sum_over_members" + market = { + row["column"] + for row in document["income_decile"]["components"] + if row["kind"] == "market" + } + assert set(binding["concepts"]) == { + f"fact:person.{column}" for column in market + } + assert binding["transform"] == {"kind": "scaled_sum", "factor": 1 / 52} + assert "annual gross income" in binding["note"] + rental = frame_concepts.concept("fact:person.rental_income") + pension = frame_concepts.concept("fact:person.private_pension_income") + assert callable(frame_concepts._amount) + assert "net of expenses" in rental.definition + assert "before tax" not in rental.definition.lower() + assert "before tax" not in pension.definition.lower() + assert '_amount("rental_income")' in disclosure["evidence"] + assert '_amount("private_pension_income")' in disclosure["evidence"] + + def test_no_resource_cites_a_repository_line_or_unpinned_foreign_line(self) -> None: + offenders = { + name: hits + for name, payload in _package_payloads().items() + if (hits := unpinned_line_citations(payload)) + } + assert offenders == {} + + def test_the_foreign_line_citations_are_scanned_and_covered_by_their_pins( + self, + ) -> None: + bridge = _load("as_rate_bridge.json") + assert {name for name, _ in source_line_citations(bridge)} == {"run.py"} + assert commit_pinned_sources(bridge) == { + "nz-lane/emtr_reproduction/run.py", + "conformance/executable/nz-treasury-incomeexplorer/requests.json", + } + assert bridge["source"]["commit"] == "ba6e7d6e4f488fdb2762dfb9d91c90f3238b719c" + assert unpinned_line_citations(bridge) == [] + # The prose-only plan_citation names no file, so it remains out of scope. + assert source_line_citations(bridge["source"]["plan_citation"]) == [] + closure = _load("axiom_input_closure.json") + expected = { + "src/rulespec.rs", + "src/dense.rs", + "src/formula.rs", + "python-ext/src/lib.rs", + } + assert {name for name, _ in source_line_citations(closure)} == expected + assert commit_pinned_sources(closure) == expected + assert unpinned_line_citations(closure) == [] + for pin in closure["engine_source_pins"]["pins"]: + assert pin["commit"] == closure["input_surface"]["engine"]["commit"] + assert re.fullmatch(r"[0-9a-f]{40}", pin["commit"]) + # Each exemption is required: removing that file's pin exposes a citation. + for name in expected: + unpinned = copy.deepcopy(closure) + unpinned["engine_source_pins"]["pins"] = [ + pin + for pin in unpinned["engine_source_pins"]["pins"] + if pin["path"] != name + ] + assert name in {path for path, _ in unpinned_line_citations(unpinned)} + + @pytest.mark.parametrize( + "replaced", + [ + "packages/microcosm-frame/src/microcosm/frame/concepts.py:880-919", + "a lone co-resident parent is always parent 1 (the pointer " + "definition, concepts.py lines 892-901; the pointer checks ...)", + "(microcosm.graph.executor._structural_columns, executor.py lines 636-643)", + "(_validate_population_declaration, executor.py lines 1550-1573)", + "the conformance harness uses 365/7 weeks (ops main run.py:168)", + "concepts.py:889-928", + "executor.py:638-645", + "executor.py:1549-1554", + "transport/population_kernels.py:208-232", + "concepts.py:900-907", + "concepts.py:1069-1083", + "adapters/policyengine_us_concepts.py:352-362", + "transport.py:843-849", + "docs/concept-coverage/policyengine-us.md:31", + "packages/microcosm-build/src/microcosm/build/transport/population_kernels.py:209-228", + "packages/microcosm-frame/src/microcosm/frame/transport.py:843-856", + "axiom_input_closure.json:1045-1052", + "axiom_input_closure.json:1183-1204", + "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json:670-688", + "concepts.py:990-1000,1040-1051", + ], + ) + def test_the_line_citations_the_package_carried_are_refused( + self, replaced: str + ) -> None: + assert unpinned_line_citations({"rule": replaced}) + + @pytest.mark.parametrize("locator", _LINE_LOCATORS) + @pytest.mark.parametrize( + "extension", ["py", "json", "yaml", "rs", "md", "toml", "csv", "ts", "h5"] + ) + def test_every_locator_spelling_covers_every_file_extension( + self, locator: str, extension: str + ) -> None: + citation = locator.format(file=f"source.{extension}", a=12, b=34) + assert source_line_citations(citation) == [(f"source.{extension}", citation)] + + @settings(max_examples=300, deadline=None) + @given( + stem=_SOURCE_STEM, + extension=_SOURCE_EXTENSION, + directory=_SOURCE_DIRECTORY, + locator=st.sampled_from(_LINE_LOCATORS), + a=st.integers(1, 99_999), + b=st.integers(1, 99_999), + prefix=_PROSE, + suffix=_PROSE, + depth=st.integers(0, 3), + as_key=st.booleans(), + ) + def test_an_unpinned_line_citation_is_refused_at_any_depth( + self, + stem: str, + extension: str, + directory: str, + locator: str, + a: int, + b: int, + prefix: str, + suffix: str, + depth: int, + as_key: bool, + ) -> None: + source = f"{directory}{stem}.{extension}" + citation = locator.format(file=source, a=a, b=b) + text = f"{prefix} {citation} {suffix}" + assert (source, text) in unpinned_line_citations(_plant(text, depth, as_key)) + + @settings(max_examples=200, deadline=None) + @given( + stem=_SOURCE_STEM, + extension=_SOURCE_EXTENSION, + locator=st.sampled_from(_LINE_LOCATORS), + a=st.integers(1, 99_999), + b=st.integers(1, 99_999), + depth=st.integers(0, 3), + as_key=st.booleans(), + commit=_HEX40_TEXT, + length=st.integers(7, 40), + bare_name=st.booleans(), + ) + def test_a_foreign_file_pin_at_a_commit_covers_its_line_citation( + self, + stem: str, + extension: str, + locator: str, + a: int, + b: int, + depth: int, + as_key: bool, + commit: str, + length: int, + bare_name: bool, + ) -> None: + source = f"foreign_citation_fixture_{stem}.{extension}" + path = f"another-repository/src/{source}" + citation = locator.format(file=source if bare_name else path, a=a, b=b) + document = { + "source": { + "repository": "Other/repository", + "path": path, + "commit": commit[:length], + }, + "notes": [_plant(citation, depth, as_key)], + } + assert source_line_citations(document) + assert unpinned_line_citations(document) == [] + + @settings(max_examples=200, deadline=None) + @given( + stem=_SOURCE_STEM, + other=_SOURCE_STEM, + extension=_SOURCE_EXTENSION, + locator=st.sampled_from(_LINE_LOCATORS), + a=st.integers(1, 99_999), + b=st.integers(1, 99_999), + commit=_HEX40_TEXT, + branch=st.sampled_from(_BRANCH_NAMES), + pin_other_file=st.booleans(), + ) + def test_another_file_or_invalid_commit_does_not_cover_a_citation( + self, + stem: str, + other: str, + extension: str, + locator: str, + a: int, + b: int, + commit: str, + branch: str, + pin_other_file: bool, + ) -> None: + source = f"foreign_citation_fixture_{stem}.{extension}" + if pin_other_file: + assume(other != stem) + pin = { + "path": f"foreign_citation_fixture_{other}.{extension}", + "commit": commit, + } + else: + pin = {"path": source, "commit": branch} + citation = locator.format(file=source, a=a, b=b) + assert (source, citation) in unpinned_line_citations( + {"source": pin, "note": citation} + ) + + def test_a_different_directory_does_not_pin_the_same_basename(self) -> None: + citation = "other/src/foreign_citation_fixture.rs:12-34" + assert unpinned_line_citations( + { + "source": { + "path": "first/src/foreign_citation_fixture.rs", + "commit": "abcdef0", + }, + "note": citation, + } + ) == [("other/src/foreign_citation_fixture.rs", citation)] + + @pytest.mark.parametrize( + "source", + [ + "concepts.py", + "packages/microcosm-frame/src/microcosm/frame/concepts.py", + "axiom_input_closure.json", + "docs/concept-coverage/policyengine-us.md", + ], + ) + def test_a_pin_never_exempts_a_repository_file(self, source: str) -> None: + citation = f"{source}:12-34" + assert unpinned_line_citations( + { + "source": {"path": source, "commit": "abcdef0"}, + "note": citation, + } + ) == [(source, citation)] + + @settings(max_examples=200, deadline=None) + @given( + stem=_SOURCE_STEM, + extension=_SOURCE_EXTENSION, + directory=_SOURCE_DIRECTORY, + locator=st.sampled_from(_SYMBOL_LOCATORS), + symbol=_SOURCE_STEM, + prefix=_PROSE, + suffix=_PROSE, + ) + def test_a_symbol_citation_is_not_a_line_citation( + self, + stem: str, + extension: str, + directory: str, + locator: str, + symbol: str, + prefix: str, + suffix: str, + ) -> None: + citation = locator.format(file=f"{directory}{stem}.{extension}", symbol=symbol) + assert source_line_citations({"rule": f"{prefix} {citation} {suffix}"}) == [] + + @settings(max_examples=200, deadline=None) + @given( + stem=_SOURCE_STEM, + key=_SOURCE_STEM, + index=st.integers(0, 9999), + depth=st.integers(0, 3), + as_key=st.booleans(), + ) + def test_a_json_pointer_citation_is_not_a_line_citation( + self, + stem: str, + key: str, + index: int, + depth: int, + as_key: bool, + ) -> None: + citation = f"{stem}.json#/entries/{index}/{key}" + assert source_line_citations(_plant(citation, depth, as_key)) == [] + assert json_pointer_citations(_plant(citation, depth, as_key)) == [ + (f"{stem}.json", f"/entries/{index}/{key}") + ] + + def test_every_json_pointer_citation_in_the_package_resolves(self) -> None: + citations = { + citation + for payload in _package_payloads().values() + for citation in json_pointer_citations(payload) + } + assert { + ("axiom_input_closure.json", "/entries/99"), + ("axiom_input_closure.json", "/entries/115"), + } <= citations + for file, pointer in sorted(citations): + resolve_json_pointer(file, pointer) + + @pytest.mark.parametrize( + ("file", "pointer", "error"), + [ + ("axiom_input_closure.json", "/entries/no_such_record", ValueError), + ("axiom_input_closure.json", "/no_such_key", KeyError), + ("no_such_citation_file.json", "/entries", ValueError), + ("axiom_input_closure.json", "/entries/01", ValueError), + ("axiom_input_closure.json", "/entries/999999", IndexError), + ("axiom_input_closure.json", "/bad~2escape", ValueError), + ], + ) + def test_a_missing_or_invalid_json_pointer_raises( + self, file: str, pointer: str, error: type[Exception] + ) -> None: + with pytest.raises(error): + resolve_json_pointer(file, pointer) + + def test_executor_symbols_still_make_unit_ids_and_memberships_structural( + self, + ) -> None: + from microcosm.graph import executor + + schema = SimpleNamespace( + entities=("person", "family"), + person_entity="person", + group_entities=("family",), + entity_id_column=lambda entity: f"{entity}_id", + membership_column=lambda entity: f"person_{entity}_id", + ) + frame = SimpleNamespace(schema=schema) + assert executor._structural_columns(frame, "family") == ["family_id"] + assert executor._structural_columns(frame, "person") == [ + "person_id", + "person_family_id", + ] + for entity, column in [("family", "family_id"), ("person", "person_family_id")]: + node = SimpleNamespace( + id="citation-fixture", + weights=None, + structural=executor.StructuralDelta.NONE, + outputs=[SimpleNamespace(entity=entity, column=column)], + ) + with pytest.raises( + executor.NodeRejected, match="cannot own structural column" + ): + executor._validate_population_declaration( + node, SimpleNamespace(frame=frame) + ) + + def test_benefit_unit_rule_cites_the_relationship_pointers_by_symbol( + self, + ) -> None: + rule = _load("benefit_unit_rule.json") + source = inspect.getsource(frame_concepts) + block = source.split("# --- Relationships", 1)[1].split("# --- ", 1)[0] + block_pointers = re.findall(r'_pointer\(\s*"(\w+)"', block) + assert block_pointers == [ + "partner_person_id", + "parent_1_person_id", + "parent_2_person_id", + "reference_person_id", + ] + pointers = rule["pointer_concepts"] + named = [ + pointers["partner"], + *pointers["parents"], + pointers["household_reference_person"], + ] + assert named == block_pointers + assert pointers["citation"] == ( + "packages/microcosm-frame/src/microcosm/frame/concepts.py, the " + f'"# --- Relationships" block: _pointer("{block_pointers[0]}") ' + f'through _pointer("{block_pointers[-1]}")' + ) + cited = { + name + for text in _nested_strings(rule) + for name in re.findall(r'_pointer\("(\w+)"\)', text) + } + assert cited == { + "partner_person_id", + "parent_1_person_id", + "reference_person_id", + } + (child,) = [ + row for row in rule["composition"] if row["member"] == "dependent_child" + ] + assert 'concepts.py _pointer("parent_1_person_id")' in child["rule"] + parent_1 = frame_concepts.concept("fact:person.parent_1_person_id") + assert "always parent 1" in parent_1.definition + assert callable(frame_concepts._check_pointers) + + def test_every_dotted_microcosm_symbol_in_the_package_resolves(self) -> None: + named = { + dotted + for payload in _package_payloads().values() + for dotted in dotted_symbol_citations(payload) + } + assert { + "microcosm.graph.executor._structural_columns", + "microcosm.graph.executor._validate_population_declaration", + } <= named + unresolved = [] + for dotted in sorted(named): + try: + resolve_dotted_symbol(dotted) + except (ImportError, AttributeError) as error: + unresolved.append(f"{dotted}: {error}") + assert unresolved == [] + + @pytest.mark.parametrize( + ("dotted", "error"), + [ + ("microcosm.graph.executor._no_such_symbol", AttributeError), + ("microcosm.no_such_shard.module.symbol", ModuleNotFoundError), + ], + ) + def test_an_unresolvable_dotted_symbol_raises( + self, dotted: str, error: type[Exception] + ) -> None: + with pytest.raises(error): + resolve_dotted_symbol(dotted) + + # --------------------------------------------------------------------------- # Remaining resources # --------------------------------------------------------------------------- diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 17cf2db3aa..33496c2392 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "3208901a27e6f09885e5990517c80e2d418425d937aa33fb0f6dacbbf290dd23", + "fingerprint": "5e97ffb44bc37ebd3809e7f6cf2ad253ef4308339338b96b4bd78746a9c1888d", "gate_ids": [ "calibration_per_family_fit", "national_and_region_admin_aggregates", @@ -41,7 +41,7 @@ "as_rate_bridge.json": "c6e21af7b090283431973a5610cc3adf5d783a4a01fb0051d5dc6c8347eeabb0", "axiom_input_closure.json": "c3c164f437b05d598af7e604d2cb46365a8323a8043da49b08c5e0cdf32e2ec1", "axiom_rules_bindings.json": "72905af77132ff6a3c01ada4b6210be4c369d9c6dc49dc8c1b673e4bac0b5fb1", - "benefit_unit_rule.json": "5c1cf16daaae482ea3ec53495f4c2e4ae452d928c6240a5284dbbf30cd2dfe37", + "benefit_unit_rule.json": "a9e8cbfd38a64d2f8db0002fe8c31bb5a18fb34dbe59fa943336487ce95e2ca8", "chronicle_feed.json": "68eb7af364bcdcfa7490af38f66dda0622aa97d77dd9841c646b7f225985680f", "country_package.json": "97ef40edd0fa0e22a3fe534306474a908c7f3ad37f46be7e245147d0f9036712", "currency_bridge.json": "aa6a4b286a6a1a015088f32ac013148553422c3d45eeef7d7a15af08388857ca", @@ -51,8 +51,8 @@ "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", "precal_references.json": "43b13849e3d74ebca7d7a9d46817463e3ad1333c3aedd7a3eac39e870510fba0", "release_contract.json": "6cd5e3a4a2bf30e4025e04e964c393b24040937d13db7e6a07b3966a55f3b114", - "reporting_classifiers.json": "b5af9e4737af4717d86aedfe651df8b3c18c3875763726e7a6a0b9b1168ed133", - "scenarios.json": "30f96f3063773d77e3c70119c8f5d99d3528eca87428903da00ca071ab34cfaf", + "reporting_classifiers.json": "789a07acd1b6724a12bbd9584ab9fa05bb01d0b675cc3a0fbc837b066dbec49e", + "scenarios.json": "9c70241a6203b3a66dc09587c8b955d9d421bbe32f018ea1542e7639f4c7507c", "source_stages.json": "cc82c68878ac0fc07ecbd796c1a66de6f34786ef8785125046425c37f26aff31", "spec/bundle.yaml": "25ef74ecbc00c3f2ecca2d30ddd5e46a582657863c217da33ee9a396569d2cf2", "spec/catalogs.yaml": "3ab2025af39cba93ead707b9463e428430a45b30696a43d0906cbcd97ed36631", From 9813c9e19e1747dc851d170d1a6cab70faa062cd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 15 Oct 2026 00:28:19 -0400 Subject: [PATCH 2/6] Select cited JSON records by key, not array index Extend NZ JSON citations with exactly-one keyed record selectors, conjunctions, RFC 6901 escaping and pointer suffixes. Refuse record-array index traversal in both citation scanning and resolution; retain scalar-array indexing. Use unique input identities in the closure and canonical_input in Axiom bindings (engine_input is not unique). Change only the three evidence strings. Regenerate the NZ golden; only reporting_classifiers.json and fingerprint move: reporting_classifiers.json: 789a07acd1b6724a12bbd9584ab9fa05bb01d0b675cc3a0fbc837b066dbec49e -> d1a5011e2163a4ceec5f030e155a02f7bc4a3bc0ad59f06fb7a406993319512f country_fingerprint: 5e97ffb44bc37ebd3809e7f6cf2ad253ef4308339338b96b4bd78746a9c1888d -> 9259333d5b7c212209654cebabc06122a8bc2dbe2cb1c776af17c6d02f1200fe Full hashes and 8-character prefixes checked; no stale tracked pins remain. Tests: test_nz_spec_package.py 279 passed; test_country_spec.py 135 passed; test_nz_axiom_input_closure.py 17 passed, 1 skipped. Total: 431 passed, 1 skipped. Initial regressions: 20 failed on the original resolver/resources. All four required mutants fail their intended tests and are restored byte for byte (1 index, 1 wrong-record, 3 scratch duplicate, 1 exactly-one failures). Ruff check/format, CI test-layout and graph-burndown verification, and git diff --check pass. Co-Authored-By: GPT-6.1 Sol --- .../build/nz/reporting_classifiers.json | 6 +- .../shared/test_nz_spec_package.py | 266 ++++++++++++++++-- .../tests/golden/nz_country_spec.json | 4 +- 3 files changed, 243 insertions(+), 33 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json b/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json index 392adcf03f..eb5e7411c7 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json @@ -260,7 +260,7 @@ "disclosures": [ { "note": "NZ Super is overstated for single people who share a home because the shares-residence input defaults false; this moves their deciles upward.", - "evidence": "axiom_input_closure.json#/entries/99", + "evidence": "axiom_input_closure.json#/entries[input=nz_super_shares_residence_with_person_at_least_adult_age]", "citation": { "item": "A1", "source": "docs/nz-reporting-classifiers-method.md#a1-income-decile-provisional" @@ -268,7 +268,7 @@ }, { "note": "Market-income components follow the existing family gross-income concept mapping. Rental income is after business expenses. The rental and private-pension concept descriptions do not explicitly say before tax; their before-tax transport interpretation must be verified before activation.", - "evidence": "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json#/bindings/40; packages/microcosm-frame/src/microcosm/frame/concepts.py _amount(\"rental_income\") and _amount(\"private_pension_income\")", + "evidence": "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json#/bindings[canonical_input=nz:statutes~1social_security~1accommodation_supplement~1core#input.accommodation_supplement_relevant_weekly_income]; packages/microcosm-frame/src/microcosm/frame/concepts.py _amount(\"rental_income\") and _amount(\"private_pension_income\")", "citation": { "item": "A1", "source": "docs/nz-reporting-classifiers-method.md#a1-income-decile-provisional" @@ -734,7 +734,7 @@ "as_non_beneficiary_audit": { "nz_super": "The existing accommodation_supplement_non_beneficiary state binding negates any_true over assigned member receipt flags, including NZ Super. A unit with any assigned NZ Super receipt is therefore not a non-beneficiary.", "veterans_pension": "The same binding omits the unsupported Veterans Pension flag, so it must be extended when that receipt input is activated. A missing Veterans Pension flag continues to block reporting rather than classify its recipients as other.", - "evidence": "axiom_input_closure.json#/entries/115", + "evidence": "axiom_input_closure.json#/entries[input=accommodation_supplement_non_beneficiary]", "citation": { "item": "A2", "source": "docs/nz-reporting-classifiers-method.md#a2-receipt-group" diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index e287568b51..0099c31d18 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -21,7 +21,16 @@ {entitlement, calibration}, and changes exactly one knob from the centre (``scenario_errors``); - no resource cites a line of a repository file, and foreign-file line - citations require a same-resource commit pin (``unpinned_line_citations``). + citations require a same-resource commit pin (``unpinned_line_citations``); +- JSON citations refuse index steps into arrays consisting of mappings. + Instead, ``file.json#/entries[input=target]`` selects exactly one record. + A selector extends an RFC 6901 token naming an array; ordinary pointer + steps may follow it, e.g. ``/entries[input=target]/state_binding/columns/0``. + Comma-separated terms are a conjunction, e.g. ``[input=target,module=nz]``. + Fields are identifiers and values are nonempty strings; brackets, commas, + equals signs and whitespace are reserved. Parse the selector before + decoding RFC 6901 ``~0`` (tilde) and ``~1`` (slash) escapes. No match or + multiple matches is an error. Indexes into scalar arrays remain valid. Differential checks compare two records of one fact: the loader's target references with a direct parse, the rules bindings' module digests with the @@ -608,7 +617,11 @@ def unpinned_line_citations(document: object) -> list[tuple[str, str]]: _DOTTED_MICROCOSM_SYMBOL = re.compile( r"(?(?:/[^\s,;()\"']*)+)") +_JSON_POINTER_CITATION = re.compile(_SOURCE_FILE + r"#(?P(?:/[^\s;()\"']*)+)") +_JSON_RECORD_SELECTOR = re.compile(r"(?P[^\[\]]+)\[(?P[^\[\]]+)\]") +_JSON_SELECTOR_TERM = re.compile( + r"(?P[A-Za-z_][A-Za-z0-9_]*)=(?P[^\[\],=\s]+)" +) def resolve_dotted_symbol(dotted: str) -> object: @@ -655,15 +668,20 @@ def dotted_symbol_citations(value: object) -> set[str]: def json_pointer_citations(value: object) -> list[tuple[str, str]]: - return [ - (match["file"], match["pointer"]) + """Find and validate every citation, including nested mapping keys.""" + + citations = [ + (match["file"], match["pointer"].rstrip(",")) for text in _nested_strings(value) for match in _JSON_POINTER_CITATION.finditer(text) ] + for citation in citations: + resolve_json_pointer(*citation) + return citations def resolve_json_pointer(file: str, pointer: str) -> object: - """Resolve an RFC 6901 pointer in one existing checkout JSON resource.""" + """Resolve a pointer with exactly-one keyed selectors; refuse record indexes.""" relative = str(PurePosixPath(file)) candidates = { @@ -687,15 +705,51 @@ def resolve_json_pointer(file: str, pointer: str) -> object: for token in pointer.split("/")[1:]: if re.search(r"~(?![01])", token): raise ValueError(f"Invalid JSON pointer escape: {pointer}") - key = token.replace("~1", "/").replace("~0", "~") + selector: dict[str, str] | None = None + key = token + if "[" in token or "]" in token: + match = _JSON_RECORD_SELECTOR.fullmatch(token) + if match is None: + raise ValueError(f"Invalid JSON record selector: {token}") + key = match["key"] + selector = {} + for term in match["terms"].split(","): + pair = _JSON_SELECTOR_TERM.fullmatch(term) + if pair is None or pair["field"] in selector: + raise ValueError(f"Invalid JSON record selector: {token}") + selector[pair["field"]] = ( + pair["value"].replace("~1", "/").replace("~0", "~") + ) + key = key.replace("~1", "/").replace("~0", "~") if isinstance(selected, list): if not re.fullmatch(r"0|[1-9][0-9]*", key): raise ValueError(f"Invalid JSON array index: {key}") + if all(isinstance(record, Mapping) for record in selected): + raise ValueError( + f"JSON citation indexes a record array: {file}#{pointer}" + ) selected = selected[int(key)] elif isinstance(selected, Mapping): selected = selected[key] else: raise TypeError(f"JSON pointer traverses a scalar: {file}#{pointer}") + if selector is not None: + if not isinstance(selected, list) or not all( + isinstance(record, Mapping) for record in selected + ): + raise ValueError( + f"JSON record selector needs an array of mappings: {token}" + ) + matches = [ + record + for record in selected + if all(record.get(field) == value for field, value in selector.items()) + ] + if len(matches) != 1: + raise ValueError( + f"JSON selector needs one matching record, found {len(matches)}: {token}" + ) + selected = matches[0] return selected @@ -1882,7 +1936,150 @@ def _plant(text: str, depth: int, as_key: bool) -> object: return planted +@pytest.fixture +def citation_checkout(tmp_path, monkeypatch): + """An isolated file lookup; each write replaces that scratch JSON payload.""" + + files: set[str] = set() + + def write(file: str, payload: object) -> None: + path = tmp_path / file + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload), encoding="utf-8") + files.add(file) + monkeypatch.setattr(__name__ + "._BUILD", SimpleNamespace(repository=tmp_path)) + monkeypatch.setattr(__name__ + "._repository_files", lambda: files) + + return write + + class TestSourceCitations: + @pytest.mark.parametrize( + ("pointer", "records", "message"), + [ + ("/entries[input=absent]", [{"input": "target"}], "one matching record"), + ("/entries[unknown=target]", [{"input": "target"}], "one matching record"), + ("/entries[input=target]", [{"other": "target"}], "one matching record"), + ( + "/entries[input=target]", + [{"input": "target"}, {"input": "target"}], + "one matching record", + ), + ("/entries[]", [{"input": "target"}], "selector"), + ("/entries[input]", [{"input": "target"}], "selector"), + ("/entries[=target]", [{"input": "target"}], "selector"), + ("/entries[input=]", [{"input": "target"}], "selector"), + ("/entries[input=target", [{"input": "target"}], "selector"), + ("/entries[input=target]]", [{"input": "target"}], "selector"), + ("/entries[[input=target]]", [{"input": "target"}], "selector"), + ("/entries[input=target]junk", [{"input": "target"}], "selector"), + ("/entries[input=target,]", [{"input": "target"}], "selector"), + ("/entries[input=target,input=target]", [{"input": "target"}], "selector"), + ("/entries/0", [{"input": "target"}], "record array"), + ("/entries/0/input", [{"input": "target"}], "record array"), + ], + ) + def test_invalid_record_selectors_and_record_array_indexes_are_refused( + self, citation_checkout, pointer: str, records: list, message: str + ) -> None: + citation_checkout("records.json", {"entries": records}) + with pytest.raises(ValueError, match=message): + resolve_json_pointer("records.json", pointer) + with pytest.raises(ValueError, match=message): + json_pointer_citations(_plant(f"records.json#{pointer}", 2, True)) + + def test_keyed_selector_extends_json_pointers_and_supports_conjunctions( + self, citation_checkout + ) -> None: + target = {"input": "target", "module": "nz/core~v1", "values": [7, 42]} + citation_checkout( + "records.json", + {"entries": [{"input": "target", "module": "other"}, target]}, + ) + pointer = "/entries[input=target,module=nz~1core~0v1]/values/1" + assert resolve_json_pointer("records.json", pointer) == 42 + assert json_pointer_citations(f"records.json#{pointer}; following prose") == [ + ("records.json", pointer) + ] + assert json_pointer_citations(f"records.json#{pointer}, following prose") == [ + ("records.json", pointer) + ] + + @pytest.mark.parametrize( + "entries", [{"input": "target"}, ["target"], [{"input": "target"}, 1]] + ) + def test_record_selector_requires_an_array_of_mappings( + self, citation_checkout, entries: object + ) -> None: + citation_checkout("records.json", {"entries": entries}) + pointer = "/entries[input=target]" + with pytest.raises(ValueError, match="array of mappings"): + resolve_json_pointer("records.json", pointer) + with pytest.raises(ValueError, match="array of mappings"): + json_pointer_citations(f"records.json#{pointer}") + + def test_scalar_array_indexes_still_check_bounds(self, citation_checkout) -> None: + citation_checkout("records.json", {"values": [7, 42]}) + assert resolve_json_pointer("records.json", "/values/1") == 42 + with pytest.raises(IndexError): + resolve_json_pointer("records.json", "/values/2") + + def test_keyed_selector_is_independent_of_record_position( + self, citation_checkout + ) -> None: + @settings(max_examples=100, deadline=None) + @given(keys=st.lists(_SOURCE_STEM, unique=True, max_size=25), data=st.data()) + def check(keys: list[str], data) -> None: + position = data.draw(st.integers(0, len(keys))) + records = [{"input": f"other_{key}", "value": key} for key in keys] + target = {"input": "target", "value": {"answer": 42}} + records.insert(position, target) + citation_checkout("records.json", {"entries": records}) + pointer = "/entries[input=target]" + assert resolve_json_pointer("records.json", pointer) == target + assert resolve_json_pointer("records.json", pointer + "/value/answer") == 42 + assert json_pointer_citations(f"records.json#{pointer}") == [ + ("records.json", pointer) + ] + with pytest.raises(ValueError, match="record array"): + resolve_json_pointer("records.json", f"/entries/{position}") + with pytest.raises(ValueError, match="record array"): + json_pointer_citations(f"records.json#/entries/{position}") + + check() + + @pytest.mark.parametrize( + "disclosure", ["sharing_home", "non_beneficiary", "market_income"] + ) + def test_keyed_citations_survive_a_record_inserted_before_the_target( + self, citation_checkout, disclosure: str + ) -> None: + reporting = _load("reporting_classifiers.json") + if disclosure == "market_income": + file = "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json" + original = json.loads( + (_BUILD.repository / file).read_text(encoding="utf-8") + ) + array, field = "bindings", "canonical_input" + expected_key = "nz:statutes/social_security/accommodation_supplement/core#input.accommodation_supplement_relevant_weekly_income" + evidence = reporting["disclosures"][1]["evidence"] + else: + file, array, field = "axiom_input_closure.json", "entries", "input" + original = _load(file) + if disclosure == "sharing_home": + expected_key = ( + "nz_super_shares_residence_with_person_at_least_adult_age" + ) + evidence = reporting["disclosures"][0]["evidence"] + else: + expected_key = "accommodation_supplement_non_beneficiary" + evidence = reporting["as_non_beneficiary_audit"]["evidence"] + (expected,) = [row for row in original[array] if row[field] == expected_key] + original[array].insert(0, {field: "dummy_record"}) + citation_checkout(file, original) + (citation,) = json_pointer_citations(evidence) + assert resolve_json_pointer(*citation) == expected + def test_local_file_pins_cannot_hide_absolute_or_parent_path_aliases(self) -> None: relative = "packages/microcosm-frame/src/microcosm/frame/concepts.py" aliases = [ @@ -2323,27 +2520,30 @@ def test_a_symbol_citation_is_not_a_line_citation( citation = locator.format(file=f"{directory}{stem}.{extension}", symbol=symbol) assert source_line_citations({"rule": f"{prefix} {citation} {suffix}"}) == [] - @settings(max_examples=200, deadline=None) - @given( - stem=_SOURCE_STEM, - key=_SOURCE_STEM, - index=st.integers(0, 9999), - depth=st.integers(0, 3), - as_key=st.booleans(), - ) def test_a_json_pointer_citation_is_not_a_line_citation( - self, - stem: str, - key: str, - index: int, - depth: int, - as_key: bool, + self, citation_checkout ) -> None: - citation = f"{stem}.json#/entries/{index}/{key}" - assert source_line_citations(_plant(citation, depth, as_key)) == [] - assert json_pointer_citations(_plant(citation, depth, as_key)) == [ - (f"{stem}.json", f"/entries/{index}/{key}") - ] + @settings(max_examples=200, deadline=None) + @given( + stem=_SOURCE_STEM, + key=_SOURCE_STEM, + index=st.integers(0, 9999), + depth=st.integers(0, 3), + as_key=st.booleans(), + ) + def check(stem: str, key: str, index: int, depth: int, as_key: bool) -> None: + citation_checkout( + f"{stem}.json", {"entries": {key: list(range(index + 1))}} + ) + pointer = f"/entries/{key}/{index}" + citation = f"{stem}.json#{pointer}" + assert source_line_citations(_plant(citation, depth, as_key)) == [] + assert json_pointer_citations(_plant(citation, depth, as_key)) == [ + (f"{stem}.json", pointer) + ] + assert resolve_json_pointer(f"{stem}.json", pointer) == index + + check() def test_every_json_pointer_citation_in_the_package_resolves(self) -> None: citations = { @@ -2352,8 +2552,18 @@ def test_every_json_pointer_citation_in_the_package_resolves(self) -> None: for citation in json_pointer_citations(payload) } assert { - ("axiom_input_closure.json", "/entries/99"), - ("axiom_input_closure.json", "/entries/115"), + ( + "axiom_input_closure.json", + "/entries[input=nz_super_shares_residence_with_person_at_least_adult_age]", + ), + ( + "axiom_input_closure.json", + "/entries[input=accommodation_supplement_non_beneficiary]", + ), + ( + "packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json", + "/bindings[canonical_input=nz:statutes~1social_security~1accommodation_supplement~1core#input.accommodation_supplement_relevant_weekly_income]", + ), } <= citations for file, pointer in sorted(citations): resolve_json_pointer(file, pointer) @@ -2365,7 +2575,7 @@ def test_every_json_pointer_citation_in_the_package_resolves(self) -> None: ("axiom_input_closure.json", "/no_such_key", KeyError), ("no_such_citation_file.json", "/entries", ValueError), ("axiom_input_closure.json", "/entries/01", ValueError), - ("axiom_input_closure.json", "/entries/999999", IndexError), + ("axiom_input_closure.json", "/entries/999999", ValueError), ("axiom_input_closure.json", "/bad~2escape", ValueError), ], ) diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 33496c2392..eae09b4580 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "5e97ffb44bc37ebd3809e7f6cf2ad253ef4308339338b96b4bd78746a9c1888d", + "fingerprint": "9259333d5b7c212209654cebabc06122a8bc2dbe2cb1c776af17c6d02f1200fe", "gate_ids": [ "calibration_per_family_fit", "national_and_region_admin_aggregates", @@ -51,7 +51,7 @@ "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", "precal_references.json": "43b13849e3d74ebca7d7a9d46817463e3ad1333c3aedd7a3eac39e870510fba0", "release_contract.json": "6cd5e3a4a2bf30e4025e04e964c393b24040937d13db7e6a07b3966a55f3b114", - "reporting_classifiers.json": "789a07acd1b6724a12bbd9584ab9fa05bb01d0b675cc3a0fbc837b066dbec49e", + "reporting_classifiers.json": "d1a5011e2163a4ceec5f030e155a02f7bc4a3bc0ad59f06fb7a406993319512f", "scenarios.json": "9c70241a6203b3a66dc09587c8b955d9d421bbe32f018ea1542e7639f4c7507c", "source_stages.json": "cc82c68878ac0fc07ecbd796c1a66de6f34786ef8785125046425c37f26aff31", "spec/bundle.yaml": "25ef74ecbc00c3f2ecca2d30ddd5e46a582657863c217da33ee9a396569d2cf2", From b1be0d975222add0e7cc2fc1bdd3eba6cadefa86 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 15 Oct 2026 02:21:22 -0400 Subject: [PATCH 3/6] Refuse backtick-wrapped and L-prefixed line citations Share the file-line locator grammar between regular and extensionless files. Recognize Markdown and quote wrappers, forward/reverse L locators, punctuation, and range/list spellings. Document recognized forms and scope. Add deep mapping-key/value regressions and extend Hypothesis generation across wrappers and dash variants. Preserve symbol, JSON-pointer and keyed record citations, with explicit negative controls. Validation: NZ spec 619 passed; country spec 135 passed; NZ closure 17 passed, 1 skipped. Physical control and three restored mutants fail as intended. Co-Authored-By: GPT-6.1 Sol --- .../shared/test_nz_spec_package.py | 200 ++++++++++++++++-- 1 file changed, 182 insertions(+), 18 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index 0099c31d18..05959eedf7 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -37,6 +37,28 @@ committed Axiom input surface, and the export contract's formula-owned list with the bound variables, and source citations with the code and JSON nodes they identify. + +Recognized line citations (for any extension, or known checkout filenames +such as LICENSE and .gitignore): + +- The filename may be plain, backtick-wrapped, single/double/curly-quoted, + inline Markdown link text (including backticks), or a reference link. +- Forward: file:12, file:L12, file:L12 (full-width colon), file#L12, + file @ L12, file L12, file, L12, file (L12), file [L12], file line 12, + file lines L12-L20, file l.12, file ll.12-20, and file at/on line 12. + Locators may themselves be quoted or backtick-wrapped. +- Reverse: line 12 in file, lines 12-20 of file, ll.12-20 in file, + and L12-L20 of/in file. Words ignore case; the L prefix is uppercase. +- Numbers may form comma lists or ranges with hyphen, en dash, em dash, + or "to", with optional spaces and an L on either endpoint. + +Bare numbers in prose, unknown extensionless names, spelled-out numbers, +HTML/entity-encoded markup and arbitrary Markdown rendering are out of scope: +the guard scans parsed resource strings for explicit file/line locators. +Lowercase l12 is ambiguous with a Python identifier; use uppercase L12 or an +explicit line/l./ll. label. Symbols (including backticks), heading anchors, +JSON Pointers and keyed selectors without a line locator remain valid. +Versioned symbols such as takeup.assign@1 also remain valid. """ from __future__ import annotations @@ -501,14 +523,34 @@ def scenario_errors(document: Mapping[str, Any]) -> list[str]: r"(?P[A-Za-z0-9_./-]*[A-Za-z0-9_-]\.[A-Za-z0-9][A-Za-z0-9_-]*)" r"(?![A-Za-z0-9_.-])" ) + + +def _line_citation_patterns(source: str) -> tuple[re.Pattern[str], ...]: + """Share the documented locator grammar with extensionless filenames.""" + + quotes = r"[`'\"‘’“”]*" + # Keep a link's destination attached to its label, so a repeated filename + # in [file](file) does not produce a second, possibly different citation. + wrapped = ( + rf"\[?{quotes}{source}{quotes}" + r"(?:\](?:\((?:[^()\n]|\([^()\n]*\))*\)|\[[^\]\n]*\])?)?" + ) + number = r"L?\s*\d+(?!\w)" + span = rf"{number}(?:\s*(?:[-–—]|(?i:to))\s*{number})?" + numbers = rf"{span}(?:\s*,\s*{span})*" + label = r"(?i:lines?\b\s+|ll?\.\s*)" + forward = ( + wrapped + + rf"(?:\s*[::]\s*{quotes}(?:{label})?|\s*@\s*{quotes}(?:{label}|(?=L))|" + rf"\s*#(?=L)|\s*,?\s*[\[(]?\s*" + rf"(?:(?i:at|on)\s+)?{quotes}(?:{label}|(?=L))){quotes}{numbers}" + ) + reverse = rf"\b(?:{label}|(?=L)){quotes}{numbers}{quotes}\s+(?i:of|in)\s+" + wrapped + return (re.compile(forward), re.compile(reverse)) + + #: Keep every locator spelling from #1141, now for any file extension. -_LINE_CITATIONS = ( - re.compile(_SOURCE_FILE + r"(?:\s*:\s*|#L|\s+L|,?\s*\(?\s*[Ll]ines?\s+)\d"), - re.compile( - r"\b[Ll]ines?\s+\d+(?:\s*[-–]\s*\d+)?(?:\s*,\s*\d+)*\s+(?:of|in)\s+" - + _SOURCE_FILE - ), -) +_LINE_CITATIONS = _line_citation_patterns(_SOURCE_FILE) _COMMIT_PIN = re.compile(r"[0-9a-f]{7,40}") @@ -547,13 +589,7 @@ def _extensionless_line_patterns() -> tuple[re.Pattern[str], ...]: } alternatives = "|".join(re.escape(name) for name in sorted(names)) source = rf"(?P[A-Za-z0-9_./-]*(?:{alternatives}))(?![A-Za-z0-9_.-])" - return ( - re.compile(source + r"(?:\s*:\s*|#L|\s+L|,?\s*\(?\s*[Ll]ines?\s+)\d"), - re.compile( - r"\b[Ll]ines?\s+\d+(?:\s*[-–]\s*\d+)?(?:\s*,\s*\d+)*\s+(?:of|in)\s+" - + source - ), - ) + return _line_citation_patterns(source) def _repository_source(path: str) -> bool: @@ -1904,6 +1940,39 @@ def test_every_refusal_branch_names_the_broken_scenario( "{file} (lines {a}-{b})", "Lines {a}-{b} of {file}", "line {a} in {file}", + "{file}:L{a}", + "{file}:L{a}-L{b}", + "{file} @ L{a}", + "{file}, L{a}-L{b}", + "{file} (L{a}-L{b})", + "{file} [L{a}-L{b}]", + "{file} lines L{a}-L{b}", + "{file} ll.{a}-{b}", + "{file} at line {a}", + "{file} on lines {a}-{b}", + "lines L{a}-L{b} of {file}", + "L{a}-L{b} in {file}", + "ll.{a}-{b} in {file}", + "lines {a}, {b} in {file}", +) +_FILE_WRAPPERS = ( + "{file}", + "`{file}`", + "'{file}'", + '"{file}"', + "‘{file}’", + "“{file}”", + "[{file}](source)", + "[`{file}`](source)", + "[{file}][source]", +) +_LINE_LOCATOR_SPELLINGS = st.builds( + lambda locator, wrapper, dash: locator.replace("-", dash).replace( + "{file}", wrapper + ), + st.sampled_from(_LINE_LOCATORS), + st.sampled_from(_FILE_WRAPPERS), + st.sampled_from(("-", "–", "—", " to ")), ) #: Symbol-citation spellings, which carry no line number. _SYMBOL_LOCATORS = ( @@ -1954,6 +2023,75 @@ def write(file: str, payload: object) -> None: class TestSourceCitations: + @pytest.mark.parametrize("as_key", [False, True], ids=["value", "key"]) + @pytest.mark.parametrize("source", ["concepts.py", "LICENSE"]) + @pytest.mark.parametrize( + "spelling", + [ + pytest.param("`{file}` lines 889-928", id="backtick-forward"), + pytest.param("lines 889-928 of `{file}`", id="backtick-reverse"), + pytest.param("{file}, L889-L928", id="l-comma"), + pytest.param("{file} (L889-L928)", id="l-parentheses"), + pytest.param("line 12 in `{file}`", id="line-in-backticks"), + pytest.param("{file}:L12", id="l-colon"), + pytest.param("{file} @ L12", id="l-at"), + pytest.param("{file}#L12-L20", id="l-fragment"), + pytest.param("{file} lines 12–20", id="en-dash-forward"), + pytest.param("{file} ll. 12-20", id="ll-forward"), + pytest.param("see {file}, line 12", id="see-comma-line"), + pytest.param("{file}:12", id="fullwidth-colon"), + pytest.param("{file}:L12–L20", id="fullwidth-l-range"), + pytest.param("'{file}' lines 12-20", id="single-quote-forward"), + pytest.param("lines 12-20 of '{file}'", id="single-quote-reverse"), + pytest.param('"{file}" lines 12-20', id="double-quote-forward"), + pytest.param('lines 12-20 of "{file}"', id="double-quote-reverse"), + pytest.param("‘{file}’ lines 12-20", id="curly-single-forward"), + pytest.param("lines 12-20 of ‘{file}’", id="curly-single-reverse"), + pytest.param("“{file}” lines 12-20", id="curly-double-forward"), + pytest.param("lines 12-20 of “{file}”", id="curly-double-reverse"), + pytest.param("[{file}]({file}) lines 12-20", id="link-forward"), + pytest.param("lines 12-20 of [{file}]({file})", id="link-reverse"), + pytest.param("[`{file}`]({file}) L12", id="code-link-forward"), + pytest.param("L12 in [`{file}`]({file})", id="code-link-reverse"), + pytest.param( + "[{file}](docs/source(v1)) lines 12-20", id="link-parentheses" + ), + pytest.param("[{file}][source] lines 12-20", id="reference-link"), + pytest.param("{file}, L12", id="l-single"), + pytest.param("{file} [L12-L20]", id="l-brackets"), + pytest.param("L12-L20 in {file}", id="l-reverse"), + pytest.param("lines L12–L20 of `{file}`", id="l-en-dash-reverse"), + pytest.param("{file} lines L12-L20", id="l-labelled"), + pytest.param("{file} lines 12—20", id="em-dash-forward"), + pytest.param("lines 12—20 of {file}", id="em-dash-reverse"), + pytest.param("lines 12–20 in {file}", id="en-dash-reverse"), + pytest.param("lines 12, 14-20 of {file}", id="reverse-list"), + pytest.param("ll.12–20 in {file}", id="ll-reverse"), + pytest.param("{file} l.12", id="l-abbreviation"), + pytest.param("{file} at line 12", id="at-line"), + pytest.param("{file} on lines 12 to 20", id="on-lines-to"), + pytest.param("lines 12 to 20 of {file}", id="reverse-to"), + pytest.param("{file}, L12–L20", id="l-en-dash-forward"), + pytest.param("LINES 12-20 OF `{file}`", id="uppercase-reverse"), + pytest.param("`{file}`: `L12`", id="quoted-locator"), + pytest.param("{file} `L12`", id="quoted-bare-locator"), + pytest.param("{file} `lines 12-20`", id="quoted-labelled-locator"), + pytest.param("{file}: lines 12-20", id="colon-labelled"), + pytest.param("lines `12–20` in `{file}`", id="quoted-reverse-numbers"), + ], + ) + def test_formatted_line_citation_is_refused_at_depth( + self, spelling: str, source: str, as_key: bool + ) -> None: + citation = spelling.format(file=source) + # A local file cannot borrow an exemption from a same-document pin. + document = { + "source": {"path": source, "commit": "abcdef0"}, + "notes": _plant(citation, 3, as_key), + } + assert source_line_citations(document) == [(source, citation)] + assert unpinned_line_citations(document) == [(source, citation)] + @pytest.mark.parametrize( ("pointer", "records", "message"), [ @@ -2363,7 +2501,7 @@ def test_every_locator_spelling_covers_every_file_extension( stem=_SOURCE_STEM, extension=_SOURCE_EXTENSION, directory=_SOURCE_DIRECTORY, - locator=st.sampled_from(_LINE_LOCATORS), + locator=_LINE_LOCATOR_SPELLINGS, a=st.integers(1, 99_999), b=st.integers(1, 99_999), prefix=_PROSE, @@ -2393,7 +2531,7 @@ def test_an_unpinned_line_citation_is_refused_at_any_depth( @given( stem=_SOURCE_STEM, extension=_SOURCE_EXTENSION, - locator=st.sampled_from(_LINE_LOCATORS), + locator=_LINE_LOCATOR_SPELLINGS, a=st.integers(1, 99_999), b=st.integers(1, 99_999), depth=st.integers(0, 3), @@ -2434,7 +2572,7 @@ def test_a_foreign_file_pin_at_a_commit_covers_its_line_citation( stem=_SOURCE_STEM, other=_SOURCE_STEM, extension=_SOURCE_EXTENSION, - locator=st.sampled_from(_LINE_LOCATORS), + locator=_LINE_LOCATOR_SPELLINGS, a=st.integers(1, 99_999), b=st.integers(1, 99_999), commit=_HEX40_TEXT, @@ -2503,6 +2641,7 @@ def test_a_pin_never_exempts_a_repository_file(self, source: str) -> None: extension=_SOURCE_EXTENSION, directory=_SOURCE_DIRECTORY, locator=st.sampled_from(_SYMBOL_LOCATORS), + wrapper=st.sampled_from(_FILE_WRAPPERS), symbol=_SOURCE_STEM, prefix=_PROSE, suffix=_PROSE, @@ -2513,13 +2652,38 @@ def test_a_symbol_citation_is_not_a_line_citation( extension: str, directory: str, locator: str, + wrapper: str, symbol: str, prefix: str, suffix: str, ) -> None: - citation = locator.format(file=f"{directory}{stem}.{extension}", symbol=symbol) + source = wrapper.format(file=f"{directory}{stem}.{extension}") + citation = locator.format(file=source, symbol=symbol) assert source_line_citations({"rule": f"{prefix} {citation} {suffix}"}) == [] + @pytest.mark.parametrize("as_key", [False, True], ids=["value", "key"]) + @pytest.mark.parametrize( + "citation", + [ + "`microcosm.graph.executor._structural_columns`", + "takeup.assign@1", + "`takeup.assign` @ 1", + "`concepts.py`: `_pointer`", + "concepts.py: L12_symbol", + "concepts.py: l12", + "concepts.py: line12", + "docs/method.md#heading12", + "records.json#/entries/12", + "records.json#/entries[input=L12,module=nz~1core~0v1]/values/0", + "records.json#/entries[input=line12]", + ], + ) + def test_symbols_and_stable_records_are_not_line_citations( + self, citation: str, as_key: bool + ) -> None: + assert source_line_citations(_plant(citation, 3, as_key)) == [] + assert unpinned_line_citations(_plant(citation, 3, as_key)) == [] + def test_a_json_pointer_citation_is_not_a_line_citation( self, citation_checkout ) -> None: From deaf49f39d423e000d1ef71703c9a8593279607a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 15 Oct 2026 02:59:10 -0400 Subject: [PATCH 4/6] Attribute Markdown-link line citations to their destination Attribute link locators to the destination and every filename in the label. Exempt a citation only when all attributed files are pinned foreign sources. Keep JSON pointer and keyed-selector fragments opaque to the line scanner. Document the prose-citation threat model and exercise its in-scope examples, deep key/value regressions, and generated link attribution and exemptions. Co-Authored-By: GPT-6.1 Sol --- .../shared/test_nz_spec_package.py | 279 +++++++++++++++++- 1 file changed, 264 insertions(+), 15 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index 05959eedf7..71ed22f2d9 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -38,11 +38,18 @@ with the bound variables, and source citations with the code and JSON nodes they identify. +The line guard catches drift-prone citations that an author (human or model) +would plausibly write in NZ spec prose. It is not a defence against deliberate +obfuscation, such as spelling out line numbers or splitting them across keys. + Recognized line citations (for any extension, or known checkout filenames such as LICENSE and .gitignore): - The filename may be plain, backtick-wrapped, single/double/curly-quoted, - inline Markdown link text (including backticks), or a reference link. + an inline Markdown link's destination or text (including backticks), an + image link, or reference-link text. A link attributes its locator to the + destination (without query or fragment) and every filename in its label; + all must be pinned foreign files for an exemption. - Forward: file:12, file:L12, file:L12 (full-width colon), file#L12, file @ L12, file L12, file, L12, file (L12), file [L12], file line 12, file lines L12-L20, file l.12, file ll.12-20, and file at/on line 12. @@ -52,12 +59,61 @@ - Numbers may form comma lists or ranges with hyphen, en dash, em dash, or "to", with optional spaces and an L on either endpoint. -Bare numbers in prose, unknown extensionless names, spelled-out numbers, -HTML/entity-encoded markup and arbitrary Markdown rendering are out of scope: -the guard scans parsed resource strings for explicit file/line locators. +In-scope examples: + file:12 + file:L12 + file:L12 + file#L12 + file @ L12 + file L12 + file, L12 + file (L12) + file [L12] + file line 12 + file lines L12-L20 + file l.12 + file ll.12-20 + file at line 12 + file on line 12 + line 12 in file + lines 12-20 of file + ll.12-20 in file + L12-L20 of file + file:12,14-20 + file lines 12–20 + file lines 12—20 + file lines 12 to 20 + `file` lines 12-20 + 'file' lines 12-20 + "file" lines 12-20 + ‘file’ lines 12-20 + “file” lines 12-20 + file: `L12` + file: 'line 12' + [file](file) lines 12-20 + [`file`](file) L12 + [file][source] lines 12-20 + [the source](file) lines 12-20 + lines 12-20 of [the source](file) + [upstream.rs](file) L12 + [the source](file#L12) + [the source](file:L12) + [file:L12](upstream.rs) + [the source](file?raw=1) L12 + ![x](file) L5 + LICENSE:12 + .gitignore:12 + +Out of scope: bare prose numbers ("line 12" without a file), unknown +extensionless names ("unknown_source:12"), spelled-out numbers ("file line +twelve"), locators split across keys or values ({"file": "line", "n": 12}), +HTML/entity-encoded markup ("file:12"), percent-encoded filenames, +escaped/nested Markdown labels, link titles, and arbitrary Markdown rendering +or reference-link destination lookup. The guard scans parsed resource strings +for explicit file/line locators. Lowercase l12 is ambiguous with a Python identifier; use uppercase L12 or an explicit line/l./ll. label. Symbols (including backticks), heading anchors, -JSON Pointers and keyed selectors without a line locator remain valid. +JSON Pointers and keyed selectors remain opaque, including locator-like tokens. Versioned symbols such as takeup.assign@1 also remain valid. """ @@ -529,8 +585,7 @@ def _line_citation_patterns(source: str) -> tuple[re.Pattern[str], ...]: """Share the documented locator grammar with extensionless filenames.""" quotes = r"[`'\"‘’“”]*" - # Keep a link's destination attached to its label, so a repeated filename - # in [file](file) does not produce a second, possibly different citation. + # Keep wrappers attached to their filename or complete Markdown link. wrapped = ( rf"\[?{quotes}{source}{quotes}" r"(?:\](?:\((?:[^()\n]|\([^()\n]*\))*\)|\[[^\]\n]*\])?)?" @@ -551,17 +606,75 @@ def _line_citation_patterns(source: str) -> tuple[re.Pattern[str], ...]: #: Keep every locator spelling from #1141, now for any file extension. _LINE_CITATIONS = _line_citation_patterns(_SOURCE_FILE) +_MARKDOWN_LINK = re.compile( + r"(?P!?\[(?P