From d480e996420f0ad3e4b709bab81f7da43cc4a1a5 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:56:15 -0400 Subject: [PATCH 1/4] refactor: one source of truth for the platform table The published-target table lived twice -- as a TypeScript literal in packages/ts/src/platforms.ts and as a hand-maintained Python subset in internals/distcheck -- and `dirsql-checks platforms-mirror` existed only to prove the copies agreed. The rows now live in packages/ts/src/platforms.json. platforms.ts imports it and casts the two node vocabularies (`NodeJS.Platform` / `NodeJS.Architecture`) at that one boundary; the distcheck flow reads the same file. The JSON sits under src/ so tsc emits it into dist/, which is what `loadNativeCore()` needs at runtime in the published package. With one copy there is nothing left to mirror, so the check, its colocated unit tests, its integration and e2e suites, its registration in the `dirsql-checks` group and its dedicated workflow are deleted. No test assertion changed. Source lines: -338 across the scan roots. --- .github/workflows/internals-distcheck-ci.yml | 2 + .github/workflows/platforms-mirror-check.yml | 44 ----- internals/checks/src/checks/cli.py | 2 - internals/checks/src/checks/cli_test.py | 4 - .../src/checks/platforms_mirror/__init__.py | 0 .../src/checks/platforms_mirror/as_json.py | 15 -- .../checks/platforms_mirror/as_json_test.py | 28 --- .../checks/platforms_mirror/assigned_names.py | 16 -- .../platforms_mirror/assigned_names_test.py | 28 --- .../src/checks/platforms_mirror/call_name.py | 12 -- .../checks/platforms_mirror/call_name_test.py | 25 --- .../checks/src/checks/platforms_mirror/cli.py | 29 --- .../src/checks/platforms_mirror/cli_test.py | 34 ---- .../platforms_mirror/dataclass_fields.py | 28 --- .../platforms_mirror/dataclass_fields_test.py | 36 ---- .../src/checks/platforms_mirror/decide.py | 22 --- .../checks/platforms_mirror/decide_test.py | 24 --- .../checks/platforms_mirror/field_problems.py | 26 --- .../platforms_mirror/field_problems_test.py | 49 ------ .../src/checks/platforms_mirror/gate.py | 46 ----- .../src/checks/platforms_mirror/gate_test.py | 100 ----------- .../src/checks/platforms_mirror/literal.py | 18 -- .../checks/platforms_mirror/literal_test.py | 29 --- .../checks/platforms_mirror/missing_rows.py | 19 -- .../platforms_mirror/missing_rows_test.py | 29 --- .../src/checks/platforms_mirror/parse.py | 20 --- .../src/checks/platforms_mirror/parse_test.py | 19 -- .../platforms_mirror/prefix_problems.py | 17 -- .../platforms_mirror/prefix_problems_test.py | 33 ---- .../src/checks/platforms_mirror/problems.py | 29 --- .../checks/platforms_mirror/problems_test.py | 61 ------- .../checks/platforms_mirror/python_table.py | 21 --- .../platforms_mirror/python_table_test.py | 42 ----- .../src/checks/platforms_mirror/read.py | 12 -- .../src/checks/platforms_mirror/read_test.py | 17 -- .../src/checks/platforms_mirror/requoted.py | 16 -- .../checks/platforms_mirror/requoted_test.py | 28 --- .../checks/src/checks/platforms_mirror/row.py | 32 ---- .../src/checks/platforms_mirror/row_test.py | 68 -------- .../src/checks/platforms_mirror/stray_rows.py | 17 -- .../platforms_mirror/stray_rows_test.py | 27 --- .../checks/platforms_mirror/table_elements.py | 25 --- .../platforms_mirror/table_elements_test.py | 48 ----- .../platforms_mirror/typescript_table.py | 36 ---- .../platforms_mirror/typescript_table_test.py | 94 ---------- .../platforms_mirror/typescript_value.py | 14 -- .../platforms_mirror/typescript_value_test.py | 36 ---- .../src/checks/platforms_mirror/vocabulary.py | 46 ----- .../platforms_mirror/vocabulary_test.py | 60 ------- .../platforms_mirror/without_comments.py | 27 --- .../platforms_mirror/without_comments_test.py | 40 ----- .../checks/tests/e2e/platforms_mirror_test.py | 98 ----------- .../integration/platforms_mirror_test.py | 165 ------------------ .../src/distcheck/node_flow/platforms.py | 36 ++-- ...2026-09-05-platform-table-single-source.md | 6 + packages/ts/package.json | 16 +- packages/ts/src/platforms.json | 44 +++++ packages/ts/src/platforms.ts | 58 ++---- packages/ts/tsconfig.json | 1 + 59 files changed, 99 insertions(+), 1875 deletions(-) delete mode 100644 .github/workflows/platforms-mirror-check.yml delete mode 100644 internals/checks/src/checks/platforms_mirror/__init__.py delete mode 100644 internals/checks/src/checks/platforms_mirror/as_json.py delete mode 100644 internals/checks/src/checks/platforms_mirror/as_json_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/assigned_names.py delete mode 100644 internals/checks/src/checks/platforms_mirror/assigned_names_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/call_name.py delete mode 100644 internals/checks/src/checks/platforms_mirror/call_name_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/cli.py delete mode 100644 internals/checks/src/checks/platforms_mirror/cli_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/dataclass_fields.py delete mode 100644 internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/decide.py delete mode 100644 internals/checks/src/checks/platforms_mirror/decide_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/field_problems.py delete mode 100644 internals/checks/src/checks/platforms_mirror/field_problems_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/gate.py delete mode 100644 internals/checks/src/checks/platforms_mirror/gate_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/literal.py delete mode 100644 internals/checks/src/checks/platforms_mirror/literal_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/missing_rows.py delete mode 100644 internals/checks/src/checks/platforms_mirror/missing_rows_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/parse.py delete mode 100644 internals/checks/src/checks/platforms_mirror/parse_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/prefix_problems.py delete mode 100644 internals/checks/src/checks/platforms_mirror/prefix_problems_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/problems.py delete mode 100644 internals/checks/src/checks/platforms_mirror/problems_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/python_table.py delete mode 100644 internals/checks/src/checks/platforms_mirror/python_table_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/read.py delete mode 100644 internals/checks/src/checks/platforms_mirror/read_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/requoted.py delete mode 100644 internals/checks/src/checks/platforms_mirror/requoted_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/row.py delete mode 100644 internals/checks/src/checks/platforms_mirror/row_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/stray_rows.py delete mode 100644 internals/checks/src/checks/platforms_mirror/stray_rows_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/table_elements.py delete mode 100644 internals/checks/src/checks/platforms_mirror/table_elements_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/typescript_table.py delete mode 100644 internals/checks/src/checks/platforms_mirror/typescript_table_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/typescript_value.py delete mode 100644 internals/checks/src/checks/platforms_mirror/typescript_value_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/vocabulary.py delete mode 100644 internals/checks/src/checks/platforms_mirror/vocabulary_test.py delete mode 100644 internals/checks/src/checks/platforms_mirror/without_comments.py delete mode 100644 internals/checks/src/checks/platforms_mirror/without_comments_test.py delete mode 100644 internals/checks/tests/e2e/platforms_mirror_test.py delete mode 100644 internals/checks/tests/integration/platforms_mirror_test.py create mode 100644 packages/ts/changelog.d/2026-09-05-platform-table-single-source.md create mode 100644 packages/ts/src/platforms.json diff --git a/.github/workflows/internals-distcheck-ci.yml b/.github/workflows/internals-distcheck-ci.yml index 558026f4b..b4bc31a66 100644 --- a/.github/workflows/internals-distcheck-ci.yml +++ b/.github/workflows/internals-distcheck-ci.yml @@ -9,6 +9,8 @@ on: - 'internals/distcheck/src/**' - 'internals/distcheck/tests/**' - 'internals/distcheck/pyproject.toml' + # The published-target table this lane's platform lookup reads. + - 'packages/ts/src/platforms.json' # This lane passes no `config:`, so the reusable workflow's default # applies and the ROOT testing-conventions.toml governs it -- an unknown # key there fails this lane too. diff --git a/.github/workflows/platforms-mirror-check.yml b/.github/workflows/platforms-mirror-check.yml deleted file mode 100644 index c643158a5..000000000 --- a/.github/workflows/platforms-mirror-check.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Platforms Mirror Check - -# `packages/ts/src/platforms.ts` is the release source of truth for the published -# `@dirsql/lib-*` sub-packages; the node distcheck flow keeps a hand-maintained -# Python subset of it. Nothing checked the correspondence, so it drifted in both -# directions -- a field on the Python side justified as mirroring a TypeScript -# property that never existed, and TypeScript-only fields with no counterpart. -# This holds the two tables to the subset on every change to either, so a new -# published target cannot be half-added. -# -# Deliberately its own workflow rather than a job in internals-distcheck-ci.yml: -# that lane's path filter does not watch packages/ts, which is exactly where the -# half-added-target case starts. - -on: - pull_request: - branches: [main] - paths: - - 'packages/ts/src/platforms.ts' - - 'internals/distcheck/src/distcheck/node_flow/platforms.py' - - 'internals/checks/src/checks/platforms_mirror/**' - - '.github/workflows/platforms-mirror-check.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - platforms-mirror: - name: Platforms mirror - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.13" - - - name: Assert the two platform tables agree on the fields they share - run: uv run --project internals/checks dirsql-checks platforms-mirror diff --git a/internals/checks/src/checks/cli.py b/internals/checks/src/checks/cli.py index 6475e34c0..9fa82339e 100644 --- a/internals/checks/src/checks/cli.py +++ b/internals/checks/src/checks/cli.py @@ -13,7 +13,6 @@ from checks.changelog_gate.cli import cli as changelog_gate from checks.declared_deps.cli import cli as declared_deps from checks.npm_binary_extension_load.cli import cli as npm_binary_extension_load -from checks.platforms_mirror.cli import cli as platforms_mirror from checks.preflight.cli import cli as preflight from checks.pytest_gate.cli import cli as pytest_gate from checks.release_globs.cli import cli as release_globs @@ -30,7 +29,6 @@ def main() -> None: main.add_command(changelog_gate, name="changelog-gate") main.add_command(declared_deps, name="declared-deps") main.add_command(npm_binary_extension_load, name="npm-binary-extension-load") -main.add_command(platforms_mirror, name="platforms-mirror") main.add_command(preflight, name="preflight") main.add_command(pytest_gate, name="pytest-gate") main.add_command(release_globs, name="release-globs") diff --git a/internals/checks/src/checks/cli_test.py b/internals/checks/src/checks/cli_test.py index 5f7266602..ea921cccf 100644 --- a/internals/checks/src/checks/cli_test.py +++ b/internals/checks/src/checks/cli_test.py @@ -34,10 +34,6 @@ def test_declared_deps_is_registered(): assert "declared-deps" in main.commands -def test_platforms_mirror_is_registered(): - assert "platforms-mirror" in main.commands - - def test_preflight_is_registered(): assert "preflight" in main.commands diff --git a/internals/checks/src/checks/platforms_mirror/__init__.py b/internals/checks/src/checks/platforms_mirror/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/internals/checks/src/checks/platforms_mirror/as_json.py b/internals/checks/src/checks/platforms_mirror/as_json.py deleted file mode 100644 index 9b290d016..000000000 --- a/internals/checks/src/checks/platforms_mirror/as_json.py +++ /dev/null @@ -1,15 +0,0 @@ -"""A comment-free object literal as JSON. - -TypeScript's bare keys and trailing commas are the only two things left between -a stripped `PLATFORMS` array and something `json` will decode. -""" - -from __future__ import annotations - -import re - - -def as_json(text: str) -> str: - """`text` with bare object keys quoted and trailing commas dropped.""" - keyed = re.sub(r"([{,]\s*)([A-Za-z_$][\w$]*)\s*:", r'\1"\2":', text) - return re.sub(r",(\s*[}\]])", r"\1", keyed) diff --git a/internals/checks/src/checks/platforms_mirror/as_json_test.py b/internals/checks/src/checks/platforms_mirror/as_json_test.py deleted file mode 100644 index 3dae05eda..000000000 --- a/internals/checks/src/checks/platforms_mirror/as_json_test.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Colocated unit tests for the JSON normalizer (isolation -- pure text in, -text out). -""" - -from checks.platforms_mirror.as_json import as_json - - -def describe_as_json(): - def it_quotes_a_bare_object_key(): - assert as_json('{nodePlatform: "linux"}') == '{"nodePlatform": "linux"}' - - def it_quotes_every_key_after_a_comma(): - assert as_json('{a: 1, b: 2}') == '{"a": 1, "b": 2}' - - def it_quotes_a_key_holding_a_dollar_sign_or_underscore(): - assert as_json("{_a$b: 1}") == '{"_a$b": 1}' - - def it_drops_a_trailing_comma_before_a_brace(): - assert as_json('{"a": 1,}') == '{"a": 1}' - - def it_drops_a_trailing_comma_before_a_bracket(): - assert as_json('["a",]') == '["a"]' - - def it_keeps_the_whitespace_that_followed_a_dropped_comma(): - assert as_json('{"a": 1,\n}') == '{"a": 1\n}' - - def it_leaves_an_already_quoted_key_alone(): - assert as_json('{"a": 1}') == '{"a": 1}' diff --git a/internals/checks/src/checks/platforms_mirror/assigned_names.py b/internals/checks/src/checks/platforms_mirror/assigned_names.py deleted file mode 100644 index a3c6b2de8..000000000 --- a/internals/checks/src/checks/platforms_mirror/assigned_names.py +++ /dev/null @@ -1,16 +0,0 @@ -"""The module-level names a statement binds.""" - -from __future__ import annotations - -import ast - - -def assigned_names(node: ast.stmt) -> list[str]: - """The module-level names ``node`` binds, empty when it binds none.""" - if isinstance(node, ast.AnnAssign): - targets = [node.target] - elif isinstance(node, ast.Assign): - targets = node.targets - else: - return [] - return [target.id for target in targets if isinstance(target, ast.Name)] diff --git a/internals/checks/src/checks/platforms_mirror/assigned_names_test.py b/internals/checks/src/checks/platforms_mirror/assigned_names_test.py deleted file mode 100644 index 4c41cf4e3..000000000 --- a/internals/checks/src/checks/platforms_mirror/assigned_names_test.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Colocated unit tests for the names a module-level statement binds (isolation --- an `ast` node in, names out). -""" - -import ast - -from checks.platforms_mirror.assigned_names import assigned_names - - -def names(source: str) -> list[str]: - return assigned_names(ast.parse(source).body[0]) - - -def describe_assigned_names(): - def it_reads_an_annotated_assignment(): - assert names("PLATFORMS: tuple = ()\n") == ["PLATFORMS"] - - def it_reads_a_plain_assignment(): - assert names("PLATFORMS = ()\n") == ["PLATFORMS"] - - def it_reads_every_name_of_a_chained_assignment(): - assert names("OTHERS = PLATFORMS = ()\n") == ["OTHERS", "PLATFORMS"] - - def it_ignores_a_binding_that_is_not_a_plain_name(): - assert names("holder.PLATFORMS = ()\n") == [] - - def it_binds_nothing_for_a_statement_that_assigns_nothing(): - assert names("import os\n") == [] diff --git a/internals/checks/src/checks/platforms_mirror/call_name.py b/internals/checks/src/checks/platforms_mirror/call_name.py deleted file mode 100644 index 82d81264a..000000000 --- a/internals/checks/src/checks/platforms_mirror/call_name.py +++ /dev/null @@ -1,12 +0,0 @@ -"""The name of a table entry's call, when it has one.""" - -from __future__ import annotations - -import ast - - -def call_name(element: ast.expr): - """The name of a plain `Name(...)` call, else ``None``.""" - if isinstance(element, ast.Call) and isinstance(element.func, ast.Name): - return element.func.id - return None diff --git a/internals/checks/src/checks/platforms_mirror/call_name_test.py b/internals/checks/src/checks/platforms_mirror/call_name_test.py deleted file mode 100644 index fcaa03018..000000000 --- a/internals/checks/src/checks/platforms_mirror/call_name_test.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Colocated unit tests for reading a table entry's call name (isolation -- an -`ast` node in, a name out). -""" - -import ast - -from checks.platforms_mirror.call_name import call_name - - -def entry(source: str) -> ast.expr: - return ast.parse(source, mode="eval").body - - -def describe_call_name(): - def it_names_a_plain_call(): - assert call_name(entry("Platform('linux')")) == "Platform" - - def it_names_the_callee_rather_than_its_arguments(): - assert call_name(entry("Machine(Platform('linux'))")) == "Machine" - - def it_has_no_name_for_a_dotted_call(): - assert call_name(entry("platforms.Platform('linux')")) is None - - def it_has_no_name_for_something_that_is_not_a_call(): - assert call_name(entry("1")) is None diff --git a/internals/checks/src/checks/platforms_mirror/cli.py b/internals/checks/src/checks/platforms_mirror/cli.py deleted file mode 100644 index 46fbd28ad..000000000 --- a/internals/checks/src/checks/platforms_mirror/cli.py +++ /dev/null @@ -1,29 +0,0 @@ -"""The platforms-mirror check -- repo-only (#1004). - -Backs `dirsql-checks platforms-mirror`: the node distcheck flow's platform table -and the TypeScript release source of truth must agree on the fields they share. -""" - -from __future__ import annotations - -import click - -from checks.platforms_mirror.vocabulary import PYTHON_FILE, TYPESCRIPT_FILE -from checks.platforms_mirror.gate import run - - -@click.command() -@click.option( - "--python", - "python_path", - default=PYTHON_FILE, - help="The node distcheck flow's platform table.", -) -@click.option( - "--typescript", - "typescript_path", - default=TYPESCRIPT_FILE, - help="The release source of truth for published sub-packages.", -) -def cli(python_path: str, typescript_path: str) -> None: - raise SystemExit(run(python_path, typescript_path)) diff --git a/internals/checks/src/checks/platforms_mirror/cli_test.py b/internals/checks/src/checks/platforms_mirror/cli_test.py deleted file mode 100644 index 480aa5a91..000000000 --- a/internals/checks/src/checks/platforms_mirror/cli_test.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Colocated unit tests for the platforms-mirror command (isolation -- no -`CliRunner`). Driven through `.callback`; `run` is mocked at its import site. -""" - -from unittest import mock - -import pytest - -from checks.platforms_mirror.cli import PYTHON_FILE, TYPESCRIPT_FILE, cli - - -def invoke(**kwargs): - with mock.patch("checks.platforms_mirror.cli.run", **kwargs) as run: - with pytest.raises(SystemExit) as exc_info: - cli.callback(python_path="p.py", typescript_path="t.ts") - return run, exc_info.value.code - - -def test_exits_with_runs_return_code(): - run, code = invoke(return_value=0) - run.assert_called_once_with("p.py", "t.ts") - assert code == 0 - - -def test_propagates_a_drifted_mirror_as_exit_one(): - assert invoke(return_value=1)[1] == 1 - - -def test_defaults_to_the_two_committed_platform_tables(): - python_path, typescript_path = cli.params - assert python_path.default == PYTHON_FILE - assert typescript_path.default == TYPESCRIPT_FILE - assert PYTHON_FILE == "internals/distcheck/src/distcheck/node_flow/platforms.py" - assert TYPESCRIPT_FILE == "packages/ts/src/platforms.ts" diff --git a/internals/checks/src/checks/platforms_mirror/dataclass_fields.py b/internals/checks/src/checks/platforms_mirror/dataclass_fields.py deleted file mode 100644 index 753909fa4..000000000 --- a/internals/checks/src/checks/platforms_mirror/dataclass_fields.py +++ /dev/null @@ -1,28 +0,0 @@ -"""The mirrored field list, read off the `Platform` dataclass annotations.""" - -from __future__ import annotations - -import ast - -from .parse import CLASS_NAME, ParseError - - -def dataclass_fields(tree: ast.Module) -> list[str]: - classes = {node.name: node for node in tree.body if isinstance(node, ast.ClassDef)} - declaration = classes.get(CLASS_NAME) - if declaration is None: - raise ParseError( - f"no `class {CLASS_NAME}` at module level; this check reads the mirrored " - f"field list off its annotations." - ) - fields = [ - statement.target.id - for statement in declaration.body - if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name) - ] - if not fields: - raise ParseError( - f"class {CLASS_NAME} declares no annotated fields; this check reads the " - f"mirrored field list off them." - ) - return fields diff --git a/internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py b/internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py deleted file mode 100644 index 085fa0455..000000000 --- a/internals/checks/src/checks/platforms_mirror/dataclass_fields_test.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Colocated unit tests for the mirrored field list (isolation -- a parsed -module in, field names out; nothing here touches the repo's real platforms.py). -""" - -import ast - -import pytest - -from checks.platforms_mirror.dataclass_fields import ParseError, dataclass_fields - - -def fields(source: str) -> list[str]: - return dataclass_fields(ast.parse(source)) - - -def describe_dataclass_fields(): - def it_reads_the_annotations_in_declaration_order(): - source = "class Platform:\n node_platform: str\n node_arch: str\n slug: str\n" - assert fields(source) == ["node_platform", "node_arch", "slug"] - - def it_ignores_a_member_with_no_annotation(): - source = "class Platform:\n slug: str\n EXTRA = 1\n" - assert fields(source) == ["slug"] - - @pytest.mark.parametrize("name", ["Machine", "Target"]) - def it_rejects_a_module_whose_only_class_is_named_otherwise(name): - with pytest.raises(ParseError, match="no `class Platform`"): - fields(f"class {name}:\n slug: str\n") - - def it_rejects_a_module_with_no_class_at_all(): - with pytest.raises(ParseError, match="no `class Platform`"): - fields("PLATFORMS = ()\n") - - def it_rejects_a_platform_class_with_no_annotated_fields(): - with pytest.raises(ParseError, match="declares no annotated fields"): - fields("class Platform:\n pass\n") diff --git a/internals/checks/src/checks/platforms_mirror/decide.py b/internals/checks/src/checks/platforms_mirror/decide.py deleted file mode 100644 index a168c876f..000000000 --- a/internals/checks/src/checks/platforms_mirror/decide.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Python fields the mirror cannot source from TypeScript. - -A field outside `SHARED` has nowhere to read its value from, so it is drift by -construction: either it was never published, or the TypeScript interface grew a -property nobody added to the shared vocabulary. -""" - -from __future__ import annotations - -from .vocabulary import SHARED, TYPESCRIPT_FILE - - -def unmirrored_fields(fields) -> list[str]: - """One message per dataclass field with no TypeScript source.""" - return [ - f"Platform.{field} has no counterpart in {TYPESCRIPT_FILE}. platforms.py " - f"holds a deliberate subset of the published target ({', '.join(SHARED)}), " - f"so a field the mirror cannot source is drift: delete it, or add the " - f"property to the TypeScript `Platform` interface and to SHARED here." - for field in fields - if field not in SHARED - ] diff --git a/internals/checks/src/checks/platforms_mirror/decide_test.py b/internals/checks/src/checks/platforms_mirror/decide_test.py deleted file mode 100644 index 66b96fb23..000000000 --- a/internals/checks/src/checks/platforms_mirror/decide_test.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Colocated unit tests for the unmirrored-field verdict (isolation -- a field -list in, messages out). -""" - -from checks.platforms_mirror.decide import SHARED, unmirrored_fields - -FIELDS = list(SHARED) - - -def describe_unmirrored_fields(): - def it_passes_the_declared_subset(): - assert unmirrored_fields(FIELDS) == [] - - def it_names_a_field_with_no_typescript_source(): - (message,) = unmirrored_fields([*FIELDS, "exe"]) - assert message.startswith("Platform.exe has no counterpart in ") - assert "packages/ts/src/platforms.ts" in message - - def it_lists_the_subset_the_mirror_can_source(): - (message,) = unmirrored_fields(["exe"]) - assert "(node_platform, node_arch, slug, os, cpu)" in message - - def it_reports_every_unmirrored_field(): - assert len(unmirrored_fields(["exe", "dev"])) == 2 diff --git a/internals/checks/src/checks/platforms_mirror/field_problems.py b/internals/checks/src/checks/platforms_mirror/field_problems.py deleted file mode 100644 index 24140d981..000000000 --- a/internals/checks/src/checks/platforms_mirror/field_problems.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Per-row field comparison for the platforms-mirror check (#1004). - -Every field in `vocabulary.py`'s SHARED must hold the same value on both sides -of a target that exists in both tables. -""" - -from __future__ import annotations - -from .typescript_value import typescript_value -from .vocabulary import PYTHON_FILE, SHARED, key - - -def field_problems(python_row: dict, typescript_row: dict) -> list[str]: - """One message per shared field whose two values disagree.""" - problems = [] - for field in SHARED: - found = python_row.get(field) - expected = typescript_value(field, typescript_row) - if found != expected: - problems.append( - f"{key(python_row['node_platform'], python_row['node_arch'])}: " - f"{field} is {found!r} in platforms.py, {expected!r} in " - f"platforms.ts. platforms.ts is the release source of truth -- " - f"change {PYTHON_FILE} to match." - ) - return problems diff --git a/internals/checks/src/checks/platforms_mirror/field_problems_test.py b/internals/checks/src/checks/platforms_mirror/field_problems_test.py deleted file mode 100644 index a94620044..000000000 --- a/internals/checks/src/checks/platforms_mirror/field_problems_test.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Colocated unit tests for the platforms-mirror field comparison (isolation -- -pure dicts in, messages out). -""" - -from checks.platforms_mirror.field_problems import field_problems - -PY_LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], -} -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} - - -def describe_field_problems(): - def it_passes_a_row_that_agrees(): - assert field_problems(PY_LINUX, TS_LINUX) == [] - - def it_names_the_field_and_both_values(): - (message,) = field_problems({**PY_LINUX, "cpu": ["arm64"]}, TS_LINUX) - assert message == ( - "linux-x64: cpu is ['arm64'] in platforms.py, ['x64'] in platforms.ts. " - "platforms.ts is the release source of truth -- change " - "internals/distcheck/src/distcheck/node_flow/platforms.py to match." - ) - - def it_derives_the_slug_before_comparing_it(): - (message,) = field_problems(PY_LINUX, {**TS_LINUX, "libName": "@dirsql/lib-other"}) - assert message.startswith("linux-x64: slug is 'linux-x64-gnu' in platforms.py, 'other'") - - def it_reports_every_disagreeing_field(): - drifted = {**PY_LINUX, "cpu": ["arm64"], "slug": "nope"} - assert len(field_problems(drifted, TS_LINUX)) == 2 - - def it_reports_a_value_that_sorts_after_the_typescript_one(): - # The pair above drifts *below* platforms.ts; this one drifts above, so - # neither ordering can stand in for the inequality. - (message,) = field_problems({**PY_LINUX, "slug": "zzz"}, TS_LINUX) - assert message.startswith("linux-x64: slug is 'zzz' in platforms.py") diff --git a/internals/checks/src/checks/platforms_mirror/gate.py b/internals/checks/src/checks/platforms_mirror/gate.py deleted file mode 100644 index f7c8223e4..000000000 --- a/internals/checks/src/checks/platforms_mirror/gate.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Orchestration for the platforms-mirror check (#1004). - -Reads both platform tables off disk and holds them to the subset invariant in -`vocabulary.py`. A table this cannot parse fails the check rather than passing -on an empty read: the whole point is that nothing silently stops comparing. -""" - -from __future__ import annotations - -from collections.abc import Callable - -from .parse import ParseError -from .problems import problems -from .python_table import python_table -from .read import read -from .typescript_table import typescript_table - - -def run( - python_path: str, - typescript_path: str, - source: Callable[[str], str] = read, - echo: Callable[[str], None] = print, -) -> int: - try: - fields, python_rows = python_table(source(python_path)) - typescript_rows = typescript_table(source(typescript_path)) - except ParseError as error: - echo(f"::error::platforms-mirror could not read a platform table: {error}") - return 1 - found = problems(fields, python_rows, typescript_rows) - for problem in found: - echo(f"::error::{problem}") - if found: - echo( - f"platforms-mirror: {len(found)} problem(s). platforms.ts is the release " - f"source of truth for the published sub-packages; platforms.py mirrors the " - f"subset named in " - f"internals/checks/src/checks/platforms_mirror/vocabulary.py's SHARED." - ) - return 1 - echo( - f"ok platforms-mirror: {python_path} mirrors the shared fields of " - f"{typescript_path} across {len(python_rows)} published target(s)." - ) - return 0 diff --git a/internals/checks/src/checks/platforms_mirror/gate_test.py b/internals/checks/src/checks/platforms_mirror/gate_test.py deleted file mode 100644 index 27a524976..000000000 --- a/internals/checks/src/checks/platforms_mirror/gate_test.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Colocated unit tests for the platforms-mirror gate (#1004). - -Isolation: the reader is injected, so nothing here touches the repo's real -platforms.py / platforms.ts. The table readers and the verdict in -`problems.py` run for real, since they are pure text-in / messages-out. -""" - -import inspect - -from checks.platforms_mirror.gate import run - -PYTHON = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: - node_platform: str - node_arch: str - slug: str - os: list[str] - cpu: list[str] -{extra} - -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu", ["linux"], ["x64"]{value}), -) -''' - -TYPESCRIPT = """\ -export const PLATFORMS: readonly Platform[] = [ - { - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "x64", - libName: "@dirsql/lib-linux-x64-gnu", - os: ["linux"], - cpu: ["x64"], - libc: ["glibc"], - }, -]; -""" - - -def sources(python=None, typescript=TYPESCRIPT): - text = { - "p.py": PYTHON.format(extra="", value="") if python is None else python, - "t.ts": typescript, - } - return lambda path: text[path] - - -def invoke(**kwargs): - lines = [] - code = run("p.py", "t.ts", source=sources(**kwargs), echo=lines.append) - return code, "\n".join(lines) - - -def describe_run(): - def it_reports_the_target_count_when_the_subset_agrees(): - code, out = invoke() - assert code == 0 - assert out == ( - "ok platforms-mirror: p.py mirrors the shared fields of t.ts across " - "1 published target(s)." - ) - - def it_fails_on_a_field_with_no_typescript_counterpart(): - code, out = invoke(python=PYTHON.format(extra=" exe: bool\n", value=", False")) - assert code == 1 - assert "::error::Platform.exe has no counterpart" in out - assert "platforms-mirror: 1 problem(s)." in out - - def it_names_vocabulary_py_so_the_shared_subset_can_be_found(): - code, out = invoke(python=PYTHON.format(extra=" exe: bool\n", value=", False")) - assert code == 1 - assert "platforms_mirror/vocabulary.py's SHARED" in out - - def it_counts_every_problem_it_found(): - code, out = invoke( - python=PYTHON.format(extra=" exe: bool\n dev: bool\n", value=", False, True") - ) - assert code == 1 - assert "platforms-mirror: 2 problem(s)." in out - - def it_fails_when_the_python_table_cannot_be_read(): - code, out = invoke(python="x = 1\n") - assert code == 1 - assert "::error::platforms-mirror could not read a platform table:" in out - assert "no `class Platform`" in out - - def it_fails_when_the_typescript_table_cannot_be_read(): - code, out = invoke(typescript="export const OTHER = [];\n") - assert code == 1 - assert "could not read a platform table:" in out - assert "no `PLATFORMS" in out - - def it_defaults_to_the_reader_in_its_own_module(): - default = inspect.signature(run).parameters["source"].default - assert default.__module__ == "checks.platforms_mirror.read" diff --git a/internals/checks/src/checks/platforms_mirror/literal.py b/internals/checks/src/checks/platforms_mirror/literal.py deleted file mode 100644 index 6165bc3fb..000000000 --- a/internals/checks/src/checks/platforms_mirror/literal.py +++ /dev/null @@ -1,18 +0,0 @@ -"""One `Platform(...)` argument, read rather than evaluated. - -A row is only as trustworthy as its arguments: anything `ast` cannot constant- -fold is a `ParseError`, not a value the check would go on comparing. -""" - -from __future__ import annotations - -import ast - -from .parse import CLASS_NAME, ParseError - - -def literal(node: ast.expr): - try: - return ast.literal_eval(node) - except ValueError as error: - raise ParseError(f"a {CLASS_NAME}(...) argument is not a literal: {error}") from error diff --git a/internals/checks/src/checks/platforms_mirror/literal_test.py b/internals/checks/src/checks/platforms_mirror/literal_test.py deleted file mode 100644 index cd4fefc24..000000000 --- a/internals/checks/src/checks/platforms_mirror/literal_test.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Colocated unit tests for reading one `Platform(...)` argument (isolation -- -an `ast` node in, a value out). -""" - -import ast - -import pytest - -from checks.platforms_mirror.literal import ParseError, literal - - -def value(source: str) -> ast.expr: - return ast.parse(source, mode="eval").body - - -def describe_literal(): - def it_reads_a_string(): - assert literal(value("'linux-x64-gnu'")) == "linux-x64-gnu" - - def it_reads_a_list_of_strings(): - assert literal(value("['linux', 'darwin']")) == ["linux", "darwin"] - - def it_rejects_a_name(): - with pytest.raises(ParseError, match="a Platform\\(...\\) argument is not a literal"): - literal(value("slug")) - - def it_carries_the_reason_ast_gave(): - with pytest.raises(ParseError, match="malformed node or string"): - literal(value("slug")) diff --git a/internals/checks/src/checks/platforms_mirror/missing_rows.py b/internals/checks/src/checks/platforms_mirror/missing_rows.py deleted file mode 100644 index b11767b20..000000000 --- a/internals/checks/src/checks/platforms_mirror/missing_rows.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Published targets with no row on the Python side.""" - -from __future__ import annotations - -from .vocabulary import PYTHON_FILE, key, slug - - -def missing_rows(python_keys, typescript_rows) -> list[str]: - """One message per published target with no row in platforms.py.""" - return [ - f"{key(row['nodePlatform'], row['nodeArch'])} is published by platforms.ts " - f"but has no row in platforms.py. Add " - f"Platform({row['nodePlatform']!r}, {row['nodeArch']!r}, " - f"{slug(str(row.get('libName', '')))!r}, {row.get('os')!r}, " - f"{row.get('cpu')!r}) to PLATFORMS in {PYTHON_FILE}, or the node distcheck " - f"flow cannot resolve the new target on that host." - for row in typescript_rows - if key(row["nodePlatform"], row["nodeArch"]) not in python_keys - ] diff --git a/internals/checks/src/checks/platforms_mirror/missing_rows_test.py b/internals/checks/src/checks/platforms_mirror/missing_rows_test.py deleted file mode 100644 index e1f26129f..000000000 --- a/internals/checks/src/checks/platforms_mirror/missing_rows_test.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Colocated unit tests for the unpublished-target verdict (isolation -- pure -dicts in, messages out). -""" - -from checks.platforms_mirror.missing_rows import missing_rows - -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} - - -def describe_missing_rows(): - def it_passes_when_every_published_target_has_a_row(): - assert missing_rows({"linux-x64"}, [TS_LINUX]) == [] - - def it_suggests_the_row_to_add(): - (message,) = missing_rows(set(), [TS_LINUX]) - assert message.startswith("linux-x64 is published by platforms.ts") - assert "Platform('linux', 'x64', 'linux-x64-gnu', ['linux'], ['x64'])" in message - - def it_points_at_the_python_table_it_would_be_added_to(): - (message,) = missing_rows(set(), [TS_LINUX]) - assert "internals/distcheck/src/distcheck/node_flow/platforms.py" in message diff --git a/internals/checks/src/checks/platforms_mirror/parse.py b/internals/checks/src/checks/platforms_mirror/parse.py deleted file mode 100644 index 2572daf62..000000000 --- a/internals/checks/src/checks/platforms_mirror/parse.py +++ /dev/null @@ -1,20 +0,0 @@ -"""What both platform tables are called, and how an unreadable one fails (#1004). - -Neither side is imported: `internals/checks` depends on neither -`internals/distcheck` nor a node toolchain, and a check that executes the file -it audits can be fooled by the file. `python_table.py` reads its side out of -`ast`; `typescript_table.py` strips comments, normalizes to JSON, and hands the -result to `json`'s own decoder. - -Every shape those readers cannot read raises `ParseError` rather than returning -an empty table -- a mirror check that silently sees no rows passes forever. -""" - -from __future__ import annotations - -CLASS_NAME = "Platform" -TABLE_NAME = "PLATFORMS" - - -class ParseError(Exception): - """A platform table that could not be read in the shape this check expects.""" diff --git a/internals/checks/src/checks/platforms_mirror/parse_test.py b/internals/checks/src/checks/platforms_mirror/parse_test.py deleted file mode 100644 index 56166e3ed..000000000 --- a/internals/checks/src/checks/platforms_mirror/parse_test.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Colocated unit tests for the shared platform-table vocabulary (#1004).""" - -from checks.platforms_mirror.parse import CLASS_NAME, ParseError, TABLE_NAME - - -def describe_names(): - def it_names_the_dataclass_the_python_table_declares(): - assert CLASS_NAME == "Platform" - - def it_names_the_binding_both_sides_hold_the_table_under(): - assert TABLE_NAME == "PLATFORMS" - - -def describe_ParseError(): - def it_carries_the_message_a_reader_raised_it_with(): - assert str(ParseError("no `class Platform`")) == "no `class Platform`" - - def it_is_an_ordinary_exception(): - assert issubclass(ParseError, Exception) diff --git a/internals/checks/src/checks/platforms_mirror/prefix_problems.py b/internals/checks/src/checks/platforms_mirror/prefix_problems.py deleted file mode 100644 index cebb1e656..000000000 --- a/internals/checks/src/checks/platforms_mirror/prefix_problems.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Published targets whose `libName` no `slug` can be derived from.""" - -from __future__ import annotations - -from .vocabulary import LIB_PREFIX, TYPESCRIPT_FILE, key - - -def prefix_problems(rows) -> list[str]: - """One message per `libName` that `librarySlug()` would throw on.""" - return [ - f"{key(row.get('nodePlatform'), row.get('nodeArch'))}: libName " - f"{row.get('libName')!r} does not start with {LIB_PREFIX!r}, so " - f"`librarySlug()` throws on it and the Python `slug` cannot be derived. " - f"Fix the name in {TYPESCRIPT_FILE}." - for row in rows - if not str(row.get("libName", "")).startswith(LIB_PREFIX) - ] diff --git a/internals/checks/src/checks/platforms_mirror/prefix_problems_test.py b/internals/checks/src/checks/platforms_mirror/prefix_problems_test.py deleted file mode 100644 index a124d1d13..000000000 --- a/internals/checks/src/checks/platforms_mirror/prefix_problems_test.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Colocated unit tests for the `libName` prefix verdict (isolation -- pure -dicts in, messages out). -""" - -from checks.platforms_mirror.prefix_problems import prefix_problems - -TS_LINUX = { - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", -} -TS_WIN = { - "nodePlatform": "win32", - "nodeArch": "x64", - "libName": "@dirsql/lib-win32-x64-msvc", -} - - -def describe_prefix_problems(): - def it_passes_well_formed_names(): - assert prefix_problems([TS_LINUX, TS_WIN]) == [] - - def it_names_a_lib_name_library_slug_would_throw_on(): - (message,) = prefix_problems([{**TS_WIN, "libName": "@dirsql/win32-x64-msvc"}]) - assert message.startswith("win32-x64: libName '@dirsql/win32-x64-msvc'") - assert "@dirsql/lib-" in message - - def it_points_at_the_typescript_table_for_the_fix(): - (message,) = prefix_problems([{**TS_WIN, "libName": "nope"}]) - assert "packages/ts/src/platforms.ts" in message - - def it_names_a_row_with_no_lib_name_at_all(): - assert len(prefix_problems([{"nodePlatform": "win32", "nodeArch": "x64"}])) == 1 diff --git a/internals/checks/src/checks/platforms_mirror/problems.py b/internals/checks/src/checks/platforms_mirror/problems.py deleted file mode 100644 index e2e9d810f..000000000 --- a/internals/checks/src/checks/platforms_mirror/problems.py +++ /dev/null @@ -1,29 +0,0 @@ -"""The full platforms-mirror verdict (#1004). - -Structural disagreements first -- an unmirrored field, a malformed `libName`, a -target on only one side -- then the per-field comparison for the targets both -tables carry. -""" - -from __future__ import annotations - -from .decide import unmirrored_fields -from .field_problems import field_problems -from .missing_rows import missing_rows -from .prefix_problems import prefix_problems -from .stray_rows import stray_rows -from .vocabulary import key - - -def problems(fields, python_rows, typescript_rows) -> list[str]: - """Every way the two tables disagree, most structural first.""" - found = unmirrored_fields(fields) + prefix_problems(typescript_rows) - by_key = {key(row["nodePlatform"], row["nodeArch"]): row for row in typescript_rows} - python_keys = {key(row["node_platform"], row["node_arch"]) for row in python_rows} - found += missing_rows(python_keys, typescript_rows) - found += stray_rows(set(by_key), python_rows) - for row in python_rows: - counterpart = by_key.get(key(row["node_platform"], row["node_arch"])) - if counterpart is not None: - found += field_problems(row, counterpart) - return found diff --git a/internals/checks/src/checks/platforms_mirror/problems_test.py b/internals/checks/src/checks/platforms_mirror/problems_test.py deleted file mode 100644 index 8c7ee0cc7..000000000 --- a/internals/checks/src/checks/platforms_mirror/problems_test.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Colocated unit tests for the full platforms-mirror verdict (isolation -- pure -dicts in, messages out). -""" - -from checks.platforms_mirror.problems import problems - -FIELDS = ["node_platform", "node_arch", "slug", "os", "cpu"] - -PY_LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], -} -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} -PY_WIN = { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - "os": ["win32"], - "cpu": ["x64"], -} -TS_WIN = { - "nodePlatform": "win32", - "nodeArch": "x64", - "libName": "@dirsql/lib-win32-x64-msvc", - "os": ["win32"], - "cpu": ["x64"], -} - - -def describe_problems(): - def it_passes_two_tables_that_mirror(): - assert problems(FIELDS, [PY_LINUX, PY_WIN], [TS_LINUX, TS_WIN]) == [] - - def it_reports_the_structural_problems_before_the_field_ones(): - found = problems( - [*FIELDS, "exe"], - [PY_LINUX, {**PY_WIN, "cpu": ["arm64"]}], - [TS_LINUX, TS_WIN], - ) - assert found[0].startswith("Platform.exe") - assert found[-1].startswith("win32-x64: cpu") - - def it_reports_a_lib_name_no_slug_can_be_derived_from(): - found = problems(FIELDS, [PY_LINUX], [{**TS_LINUX, "libName": "@dirsql/linux-x64-gnu"}]) - assert found[0].startswith("linux-x64: libName '@dirsql/linux-x64-gnu'") - - def it_compares_only_the_rows_present_on_both_sides(): - found = problems(FIELDS, [PY_LINUX, PY_WIN], [TS_LINUX]) - assert len(found) == 1 - assert found[0].startswith("win32-x64 has a row in platforms.py") diff --git a/internals/checks/src/checks/platforms_mirror/python_table.py b/internals/checks/src/checks/platforms_mirror/python_table.py deleted file mode 100644 index 3f7397e43..000000000 --- a/internals/checks/src/checks/platforms_mirror/python_table.py +++ /dev/null @@ -1,21 +0,0 @@ -"""The `Platform` fields and `PLATFORMS` rows of a `platforms.py`-shaped module. - -The field list comes off the dataclass annotations and the rows off the -module-level assignment, both through `ast`: the module is never imported, so a -table that builds itself at import time is a `ParseError` rather than a read. -""" - -from __future__ import annotations - -import ast - -from .dataclass_fields import dataclass_fields -from .row import row -from .table_elements import table_elements - - -def python_table(source: str) -> tuple[list[str], list[dict]]: - """`(field names, rows)` from a `platforms.py`-shaped module.""" - tree = ast.parse(source) - fields = dataclass_fields(tree) - return fields, [row(element, fields) for element in table_elements(tree)] diff --git a/internals/checks/src/checks/platforms_mirror/python_table_test.py b/internals/checks/src/checks/platforms_mirror/python_table_test.py deleted file mode 100644 index 921987325..000000000 --- a/internals/checks/src/checks/platforms_mirror/python_table_test.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Colocated unit tests for the Python-table reader (isolation -- pure text in, -data out; nothing here touches the repo's real platforms.py). -""" - -from checks.platforms_mirror.python_table import python_table - -PYTHON = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: - node_platform: str - node_arch: str - slug: str - - -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu"), - Platform("win32", "x64", slug="win32-x64-msvc", node_arch="x64"), -) -''' - - -def describe_python_table(): - def it_reads_the_annotated_fields_and_the_rows(): - fields, rows = python_table(PYTHON) - assert fields == ["node_platform", "node_arch", "slug"] - assert rows[0] == {"node_platform": "linux", "node_arch": "x64", "slug": "linux-x64-gnu"} - - def it_reads_every_row_of_the_table(): - assert len(python_table(PYTHON)[1]) == 2 - - def it_names_each_positional_after_the_field_in_that_position(): - # The field list is what turns a row's positionals into names, so the - # two readers have to be composed in that order. - _, rows = python_table(PYTHON) - assert rows[1] == { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - } diff --git a/internals/checks/src/checks/platforms_mirror/read.py b/internals/checks/src/checks/platforms_mirror/read.py deleted file mode 100644 index e9e8c72dc..000000000 --- a/internals/checks/src/checks/platforms_mirror/read.py +++ /dev/null @@ -1,12 +0,0 @@ -"""The default reader for the platforms-mirror check (#1004). - -Split from the gate so the orchestration holds no I/O: the gate takes this as -an injected default and unit-tests against text rather than the repo's files. -""" - -from __future__ import annotations - - -def read(path: str) -> str: - with open(path, encoding="utf-8") as handle: - return handle.read() diff --git a/internals/checks/src/checks/platforms_mirror/read_test.py b/internals/checks/src/checks/platforms_mirror/read_test.py deleted file mode 100644 index d868fe66f..000000000 --- a/internals/checks/src/checks/platforms_mirror/read_test.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Colocated unit test for the platforms-mirror reader (isolation -- a scratch -file, never the repo's real platforms.py / platforms.ts). -""" - -from checks.platforms_mirror.read import read - - -def describe_read(): - def it_reads_a_file_as_utf8_text(tmp_path): - path = tmp_path / "platforms.ts" - path.write_text("// caffè\n", encoding="utf-8") - assert read(str(path)) == "// caffè\n" - - def it_reads_a_file_whole(tmp_path): - path = tmp_path / "platforms.ts" - path.write_text("one\ntwo\n", encoding="utf-8") - assert read(str(path)) == "one\ntwo\n" diff --git a/internals/checks/src/checks/platforms_mirror/requoted.py b/internals/checks/src/checks/platforms_mirror/requoted.py deleted file mode 100644 index 53e3076b1..000000000 --- a/internals/checks/src/checks/platforms_mirror/requoted.py +++ /dev/null @@ -1,16 +0,0 @@ -"""A TypeScript string literal as a JSON one. - -Single-quoted and template forms carry the same characters JSON spells with -double quotes, so re-quoting is what lets `json` read the table as data. -""" - -from __future__ import annotations - -import json - - -def requoted(text: str) -> str: - """A TypeScript string literal as a JSON one.""" - if text.startswith('"'): - return text - return json.dumps(text[1:-1].replace('\\"', '"').replace("\\'", "'")) diff --git a/internals/checks/src/checks/platforms_mirror/requoted_test.py b/internals/checks/src/checks/platforms_mirror/requoted_test.py deleted file mode 100644 index dd9cc4d09..000000000 --- a/internals/checks/src/checks/platforms_mirror/requoted_test.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Colocated unit tests for the string re-quoter (isolation -- pure text in, -text out). -""" - -from checks.platforms_mirror.requoted import requoted - - -def describe_requoted(): - def it_keeps_a_double_quoted_literal_verbatim(): - assert requoted('"linux"') == '"linux"' - - def it_rewrites_a_single_quoted_literal_as_json(): - assert requoted("'linux'") == '"linux"' - - def it_rewrites_a_template_literal_as_json(): - assert requoted("`linux`") == '"linux"' - - def it_keeps_the_contents_of_a_single_quoted_literal_whole(): - assert requoted("'aba'") == '"aba"' - - def it_unescapes_an_escaped_single_quote(): - assert requoted(r"'it\'s'") == '"it\'s"' - - def it_unescapes_an_escaped_double_quote_inside_a_template(): - assert requoted(r"`a\"b`") == '"a\\"b"' - - def it_escapes_a_double_quote_that_was_bare_in_a_single_quoted_literal(): - assert requoted("'a\"b'") == '"a\\"b"' diff --git a/internals/checks/src/checks/platforms_mirror/row.py b/internals/checks/src/checks/platforms_mirror/row.py deleted file mode 100644 index 33e59cec8..000000000 --- a/internals/checks/src/checks/platforms_mirror/row.py +++ /dev/null @@ -1,32 +0,0 @@ -"""One `Platform(...)` entry of the Python table, as a dict (#1004). - -Read statically rather than evaluated, so a row built by anything other than a -literal call -- a splat, a name, a computed value -- is a `ParseError` instead -of a row the check would go on comparing. -""" - -from __future__ import annotations - -import ast - -from .call_name import call_name -from .literal import literal -from .parse import CLASS_NAME, ParseError, TABLE_NAME - -ROW_CALLS = frozenset({CLASS_NAME}) - - -def row(element: ast.expr, fields: list[str]) -> dict: - if call_name(element) not in ROW_CALLS: - raise ParseError(f"every {TABLE_NAME} entry must be a literal `{CLASS_NAME}(...)` call.") - if len(element.args) > len(fields): - raise ParseError( - f"a {CLASS_NAME}(...) row passes {len(element.args)} positional arguments but " - f"{CLASS_NAME} declares {len(fields)} fields." - ) - values = dict(zip(fields, (literal(argument) for argument in element.args))) - for keyword in element.keywords: - if keyword.arg is None: - raise ParseError(f"a {CLASS_NAME}(...) row splats **kwargs; read it statically.") - values[keyword.arg] = literal(keyword.value) - return values diff --git a/internals/checks/src/checks/platforms_mirror/row_test.py b/internals/checks/src/checks/platforms_mirror/row_test.py deleted file mode 100644 index 0c9439a80..000000000 --- a/internals/checks/src/checks/platforms_mirror/row_test.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Colocated unit tests for reading one `Platform(...)` entry (isolation -- an -`ast` node in, a dict out; nothing here touches the repo's real platforms.py). -""" - -import ast - -import pytest - -from checks.platforms_mirror.row import ParseError, row - -FIELDS = ["node_platform", "node_arch", "slug"] - - -def entry(source: str) -> ast.expr: - """A table entry, as `ast` hands it over from the PLATFORMS assignment.""" - return ast.parse(source, mode="eval").body - - -def describe_row(): - def it_names_every_positional_after_the_field_in_that_position(): - assert row(entry('Platform("linux", "x64", "linux-x64-gnu")'), FIELDS) == { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - } - - def it_lets_keywords_override_positionals(): - source = 'Platform("win32", "ia32", slug="win32-x64-msvc", node_arch="x64")' - assert row(entry(source), FIELDS) == { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - } - - def it_reads_a_list_valued_keyword(): - assert row(entry('Platform(os=["linux", "android"])'), ["os"]) == { - "os": ["linux", "android"] - } - - def it_reads_a_row_that_fills_fewer_fields_than_the_class_declares(): - assert row(entry('Platform("linux")'), FIELDS) == {"node_platform": "linux"} - - @pytest.mark.parametrize( - "source", - ["1", "Machine('a')", "Target('a')", "platforms.Platform('a')"], - ids=["not-a-call", "machine", "target", "dotted"], - ) - def it_rejects_an_entry_that_is_not_a_literal_platform_call(source): - with pytest.raises(ParseError, match="every PLATFORMS entry must be a literal `Platform"): - row(entry(source), FIELDS) - - def it_counts_the_positionals_and_the_fields_when_there_are_too_many(): - with pytest.raises( - ParseError, match="passes 2 positional arguments but Platform declares 1 fields" - ): - row(entry('Platform("a", "b")'), ["slug"]) - - def it_rejects_a_row_that_splats_keywords(): - with pytest.raises(ParseError, match="splats"): - row(entry("Platform(**other)"), ["slug"]) - - def it_rejects_a_non_literal_positional(): - with pytest.raises(ParseError, match="not a literal"): - row(entry("Platform(slug)"), ["slug"]) - - def it_rejects_a_non_literal_keyword_value(): - with pytest.raises(ParseError, match="not a literal"): - row(entry("Platform(slug=name)"), ["slug"]) diff --git a/internals/checks/src/checks/platforms_mirror/stray_rows.py b/internals/checks/src/checks/platforms_mirror/stray_rows.py deleted file mode 100644 index 00356e9df..000000000 --- a/internals/checks/src/checks/platforms_mirror/stray_rows.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Python rows for targets the release never publishes.""" - -from __future__ import annotations - -from .vocabulary import PYTHON_FILE, TYPESCRIPT_FILE, key - - -def stray_rows(typescript_keys, python_rows) -> list[str]: - """One message per platforms.py row that is not a published target.""" - return [ - f"{key(row['node_platform'], row['node_arch'])} has a row in platforms.py " - f"but is not a published target in platforms.ts. Either add the target to " - f"PLATFORMS in {TYPESCRIPT_FILE} or drop the row from {PYTHON_FILE}: " - f"distcheck must not resolve a sub-package the release never publishes." - for row in python_rows - if key(row["node_platform"], row["node_arch"]) not in typescript_keys - ] diff --git a/internals/checks/src/checks/platforms_mirror/stray_rows_test.py b/internals/checks/src/checks/platforms_mirror/stray_rows_test.py deleted file mode 100644 index 890fdb3c6..000000000 --- a/internals/checks/src/checks/platforms_mirror/stray_rows_test.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Colocated unit tests for the unpublished-row verdict (isolation -- pure dicts -in, messages out). -""" - -from checks.platforms_mirror.stray_rows import stray_rows - -PY_LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], -} - - -def describe_stray_rows(): - def it_passes_when_every_row_is_published(): - assert stray_rows({"linux-x64"}, [PY_LINUX]) == [] - - def it_names_a_row_the_release_never_publishes(): - (message,) = stray_rows(set(), [PY_LINUX]) - assert message.startswith("linux-x64 has a row in platforms.py") - assert "packages/ts/src/platforms.ts" in message - - def it_names_both_files_the_disagreement_can_be_settled_in(): - (message,) = stray_rows(set(), [PY_LINUX]) - assert "internals/distcheck/src/distcheck/node_flow/platforms.py" in message diff --git a/internals/checks/src/checks/platforms_mirror/table_elements.py b/internals/checks/src/checks/platforms_mirror/table_elements.py deleted file mode 100644 index 5e3171bbd..000000000 --- a/internals/checks/src/checks/platforms_mirror/table_elements.py +++ /dev/null @@ -1,25 +0,0 @@ -"""The row expressions of a `platforms.py`-shaped module's `PLATFORMS` table. - -Read statically off the module-level binding, so a table that builds itself at -import time is a `ParseError` rather than a read. -""" - -from __future__ import annotations - -import ast - -from .assigned_names import assigned_names -from .parse import ParseError, TABLE_NAME - - -def table_elements(tree: ast.Module) -> list[ast.expr]: - bindings = {name: node for node in tree.body for name in assigned_names(node)} - assignment = bindings.get(TABLE_NAME) - if assignment is None: - raise ParseError(f"no module-level `{TABLE_NAME} = (...)` assignment.") - if not isinstance(assignment.value, (ast.Tuple, ast.List)): - raise ParseError( - f"{TABLE_NAME} is not a tuple or list literal; this check reads its rows " - f"statically and cannot evaluate a computed table." - ) - return list(assignment.value.elts) diff --git a/internals/checks/src/checks/platforms_mirror/table_elements_test.py b/internals/checks/src/checks/platforms_mirror/table_elements_test.py deleted file mode 100644 index 136688e57..000000000 --- a/internals/checks/src/checks/platforms_mirror/table_elements_test.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Colocated unit tests for the PLATFORMS table reader (isolation -- pure text -in, AST nodes out; nothing here touches the repo's real platforms.py). -""" - -import ast - -import pytest - -from checks.platforms_mirror.table_elements import ParseError, table_elements - - -def elements(source: str): - return table_elements(ast.parse(source)) - - -def describe_table_elements(): - def it_reads_the_rows_of_an_annotated_assignment(): - assert len(elements("PLATFORMS: tuple = (Platform('a'), Platform('b'))\n")) == 2 - - def it_reads_a_plain_assignment_without_an_annotation(): - assert len(elements("PLATFORMS = (Platform('a'),)\n")) == 1 - - def it_reads_a_list_literal(): - assert len(elements("PLATFORMS = [Platform('a')]\n")) == 1 - - def it_reads_a_table_bound_alongside_another_name(): - assert len(elements("OTHERS = PLATFORMS = (Platform('a'),)\n")) == 1 - - def it_hands_back_the_row_expressions_themselves(): - (element,) = elements("PLATFORMS = (Platform('a'),)\n") - assert isinstance(element, ast.Call) - - @pytest.mark.parametrize("name", ["OTHERS", "TARGETS"]) - def it_rejects_a_table_bound_under_another_name(name): - with pytest.raises(ParseError, match="no module-level"): - elements(f"{name} = ()\n") - - def it_rejects_a_module_that_binds_no_names_at_all(): - with pytest.raises(ParseError, match="no module-level"): - elements("import os\n") - - def it_rejects_a_computed_table(): - with pytest.raises(ParseError, match="not a tuple or list literal"): - elements("PLATFORMS = tuple(rows)\n") - - def it_names_the_table_it_could_not_find(): - with pytest.raises(ParseError, match="`PLATFORMS = "): - elements("OTHERS = ()\n") diff --git a/internals/checks/src/checks/platforms_mirror/typescript_table.py b/internals/checks/src/checks/platforms_mirror/typescript_table.py deleted file mode 100644 index 0f804c6b3..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_table.py +++ /dev/null @@ -1,36 +0,0 @@ -"""The `PLATFORMS` rows of a `platforms.ts`-shaped module. - -No node toolchain and no evaluation: the source is stripped of comments, -normalized to JSON, and handed to `json`'s own decoder, which finds the end of -the array so nothing here counts brackets. -""" - -from __future__ import annotations - -import json -import re - -from .as_json import as_json -from .parse import ParseError, TABLE_NAME -from .without_comments import without_comments - - -def typescript_table(source: str) -> list[dict]: - """Rows of the `PLATFORMS` array in a `platforms.ts`-shaped module.""" - cleaned = without_comments(source) - marker = re.search(rf"\b{TABLE_NAME}\b[^=;]*=\s*(\[)", cleaned) - if marker is None: - raise ParseError(f"no `{TABLE_NAME} = [...]` assignment in the TypeScript source.") - try: - # `raw_decode` stops at the end of the array, so nothing here has to find - # the closing bracket or care what follows it. - rows, _ = json.JSONDecoder().raw_decode(as_json(cleaned[marker.start(1) :])) - except json.JSONDecodeError as error: - raise ParseError( - f"`{TABLE_NAME}` is not a plain array of object literals ({error}); this check " - f"reads it as data and cannot evaluate spreads, computed keys or identifiers, " - f"and cannot read an unterminated array, string or comment." - ) from error - if not all(isinstance(row, dict) for row in rows): - raise ParseError(f"every `{TABLE_NAME}` entry must be an object literal.") - return rows diff --git a/internals/checks/src/checks/platforms_mirror/typescript_table_test.py b/internals/checks/src/checks/platforms_mirror/typescript_table_test.py deleted file mode 100644 index 78dd277cd..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_table_test.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Colocated unit tests for the TypeScript-table reader (isolation -- pure text -in, data out; nothing here touches the repo's real platforms.ts). -""" - -from unittest import mock - -import pytest - -from checks.platforms_mirror.typescript_table import ParseError, typescript_table - -TYPESCRIPT = """\ -// Leading comment with a "quoted" brace { and a stray ]. -export const PLATFORMS: readonly Platform[] = [ - { - /* block comment */ - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - os: ["linux"], // trailing comment - }, -]; -""" - - -def describe_typescript_table(): - def it_reads_the_array_through_comments_and_trailing_commas(): - assert typescript_table(TYPESCRIPT) == [ - { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "os": ["linux"], - } - ] - - def it_normalizes_single_quoted_strings(): - assert typescript_table("const PLATFORMS = [{ os: ['linux'] }];") == [{"os": ["linux"]}] - - def it_unwraps_a_single_quoted_string_without_trimming_its_contents(): - assert typescript_table("const PLATFORMS = [{ os: ['aba'] }];") == [{"os": ["aba"]}] - - def it_unwraps_a_template_literal(): - assert typescript_table("const PLATFORMS = [{ os: [`aba`] }];") == [{"os": ["aba"]}] - - def it_keeps_a_double_quoted_string_verbatim(): - assert typescript_table('const PLATFORMS = [{ os: ["aba"] }];') == [{"os": ["aba"]}] - - def it_finds_the_array_when_a_comment_mentions_a_bracket(): - source = "// see PLATFORMS = [ elsewhere\nconst PLATFORMS = [{ os: ['a'] }];" - assert typescript_table(source) == [{"os": ["a"]}] - - def it_keeps_an_escape_sequence_inside_a_string(): - assert typescript_table(r'const PLATFORMS = [{ os: "a\"b" }];') == [{"os": 'a"b'}] - - def it_rejects_a_source_with_no_table(): - with pytest.raises(ParseError, match="no `PLATFORMS"): - typescript_table("export const OTHER = [];\n") - - @pytest.mark.parametrize( - "source", - [ - "const PLATFORMS = [{ os: [] }\n", - "const PLATFORMS = [ /* open\n", - 'const PLATFORMS = [{ os: "open\n', - 'const PLATFORMS = [{ os: "open\\', - "const PLATFORMS = [ // open", - ], - ids=["unclosed-array", "unclosed-block-comment", "unclosed-string", "trailing-backslash", "comment-to-eof"], - ) - def it_rejects_a_table_it_cannot_read_to_the_end(source): - with pytest.raises(ParseError, match="not a plain array"): - typescript_table(source) - - def it_rejects_an_entry_that_is_not_an_object_literal(): - with pytest.raises(ParseError, match="object literal"): - typescript_table('const PLATFORMS = ["linux"];') - - def it_rejects_a_table_it_cannot_read_as_data(): - with pytest.raises(ParseError, match="not a plain array"): - typescript_table("const PLATFORMS = [{ ...spread }];") - - def it_strips_comments_through_the_helper_in_its_own_module(): - with mock.patch( - "checks.platforms_mirror.typescript_table.without_comments", - return_value='PLATFORMS = [{"os": ["a"]}]', - ) as stripper: - assert typescript_table("// only the stripper sees this") == [{"os": ["a"]}] - stripper.assert_called_once_with("// only the stripper sees this") - - def it_normalizes_to_json_through_the_helper_in_its_own_module(): - with mock.patch( - "checks.platforms_mirror.typescript_table.as_json", - return_value='[{"os": ["a"]}]', - ) as normalizer: - assert typescript_table("const PLATFORMS = [{ os: ['b'] }];") == [{"os": ["a"]}] - normalizer.assert_called_once_with("""[{ os: ["b"] }];""") diff --git a/internals/checks/src/checks/platforms_mirror/typescript_value.py b/internals/checks/src/checks/platforms_mirror/typescript_value.py deleted file mode 100644 index 594554d89..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_value.py +++ /dev/null @@ -1,14 +0,0 @@ -"""One TypeScript row's value for a Python field name.""" - -from __future__ import annotations - -from .vocabulary import DERIVE, SHARED - - -def typescript_value(field: str, row: dict): - """``row``'s value for a Python field name.""" - value = row.get(SHARED[field]) - derive = DERIVE.get(field) - if derive is None or not isinstance(value, str): - return value - return derive(value) diff --git a/internals/checks/src/checks/platforms_mirror/typescript_value_test.py b/internals/checks/src/checks/platforms_mirror/typescript_value_test.py deleted file mode 100644 index 9d91fc7b3..000000000 --- a/internals/checks/src/checks/platforms_mirror/typescript_value_test.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Colocated unit tests for reading a Python field off a TypeScript row -(isolation -- a dict in, a value out). -""" - -from checks.platforms_mirror.typescript_value import DERIVE, typescript_value - -TS_LINUX = { - "triple": "x86_64-unknown-linux-gnu", - "nodePlatform": "linux", - "nodeArch": "x64", - "libName": "@dirsql/lib-linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "libc": ["glibc"], -} - - -def describe_typescript_value(): - def it_strips_the_sub_package_prefix_off_the_slug(): - assert typescript_value("slug", TS_LINUX) == "linux-x64-gnu" - - def it_reads_other_fields_straight_through(): - assert typescript_value("cpu", TS_LINUX) == ["x64"] - - def it_leaves_a_lib_name_without_the_prefix_alone(): - assert typescript_value("slug", {"libName": "@dirsql/nope"}) == "@dirsql/nope" - - def it_derives_only_the_fields_that_need_it(): - assert set(DERIVE) == {"slug"} - - def it_does_not_strip_a_prefix_off_a_straight_read_field(): - row = {"nodePlatform": "@dirsql/lib-linux"} - assert typescript_value("node_platform", row) == "@dirsql/lib-linux" - - def it_returns_none_for_a_missing_property(): - assert typescript_value("slug", {}) is None diff --git a/internals/checks/src/checks/platforms_mirror/vocabulary.py b/internals/checks/src/checks/platforms_mirror/vocabulary.py deleted file mode 100644 index b73541814..000000000 --- a/internals/checks/src/checks/platforms_mirror/vocabulary.py +++ /dev/null @@ -1,46 +0,0 @@ -"""What the two platform tables share, and where each Python field comes from. - -`internals/distcheck`'s `Platform` dataclass is a hand-maintained subset of -`packages/ts/src/platforms.ts`, which is the release source of truth for the -published `@dirsql/lib-*` sub-packages. The subset framing is deliberate -- -distcheck needs only enough of each target to reconstruct the host's -sub-package and find its staged addon -- but it was asserted by a comment and -checked by nothing, and it drifted both ways: `exe` sat on the Python side -justified as mirroring TypeScript, which has never had the field. - -So the invariant is the subset, stated as data: `SHARED` names every Python -field and where its value comes from, both tables carry the same set of -targets, and a Python field outside `SHARED` is drift by construction. -`triple` and `libc` staying TypeScript-only is the subset working as intended, -not a gap. -""" - -from __future__ import annotations - -PYTHON_FILE = "internals/distcheck/src/distcheck/node_flow/platforms.py" -TYPESCRIPT_FILE = "packages/ts/src/platforms.ts" - -# Python field -> the TypeScript property it is sourced from. `slug` is the -# sub-package name minus its prefix, which is what `librarySlug()` returns. -SHARED = { - "node_platform": "nodePlatform", - "node_arch": "nodeArch", - "slug": "libName", - "os": "os", - "cpu": "cpu", -} -LIB_PREFIX = "@dirsql/lib-" - - -def slug(lib_name: str) -> str: - return lib_name.removeprefix(LIB_PREFIX) - - -# Python field -> how its TypeScript value is derived, for the fields that are -# not a straight read. Keyed rather than branched so the field name is never -# compared, only looked up. -DERIVE = {"slug": slug} - - -def key(node_platform: str, node_arch: str) -> str: - return f"{node_platform}-{node_arch}" diff --git a/internals/checks/src/checks/platforms_mirror/vocabulary_test.py b/internals/checks/src/checks/platforms_mirror/vocabulary_test.py deleted file mode 100644 index 90004be8b..000000000 --- a/internals/checks/src/checks/platforms_mirror/vocabulary_test.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Colocated unit tests for the shared platform vocabulary (isolation -- names -and pure lookups, no collaborators). -""" - -from checks.platforms_mirror.vocabulary import ( - DERIVE, - LIB_PREFIX, - PYTHON_FILE, - SHARED, - TYPESCRIPT_FILE, - key, - slug, -) - - -def describe_shared(): - def it_names_every_mirrored_field_and_its_typescript_source(): - assert SHARED == { - "node_platform": "nodePlatform", - "node_arch": "nodeArch", - "slug": "libName", - "os": "os", - "cpu": "cpu", - } - - def it_leaves_the_typescript_only_fields_out_of_the_subset(): - assert "triple" not in SHARED - assert "libc" not in SHARED - assert "exe" not in SHARED - - -def describe_derive(): - def it_derives_only_the_slug(): - assert set(DERIVE) == {"slug"} - assert DERIVE["slug"] is slug - - -def describe_slug(): - def it_strips_the_sub_package_prefix(): - assert slug("@dirsql/lib-darwin-arm64") == "darwin-arm64" - - def it_leaves_a_name_without_the_prefix_alone(): - assert slug("@dirsql/darwin-arm64") == "@dirsql/darwin-arm64" - - def it_reads_the_prefix_the_release_publishes_under(): - assert LIB_PREFIX == "@dirsql/lib-" - - -def describe_key(): - def it_joins_the_platform_and_the_arch_with_a_dash(): - assert key("linux", "x64") == "linux-x64" - - def it_keeps_the_platform_first(): - assert key("x64", "linux") == "x64-linux" - - -def describe_the_mirrored_files(): - def it_names_both_sides_of_the_mirror(): - assert PYTHON_FILE == "internals/distcheck/src/distcheck/node_flow/platforms.py" - assert TYPESCRIPT_FILE == "packages/ts/src/platforms.ts" diff --git a/internals/checks/src/checks/platforms_mirror/without_comments.py b/internals/checks/src/checks/platforms_mirror/without_comments.py deleted file mode 100644 index debd8d4b6..000000000 --- a/internals/checks/src/checks/platforms_mirror/without_comments.py +++ /dev/null @@ -1,27 +0,0 @@ -"""A `platforms.ts`-shaped source with its comments stripped and strings requoted. - -One alternation of "things a naive scan would cut in half": the three string -forms come first, so a `//` inside a string is matched as string rather than as -the comment that follows it. -""" - -from __future__ import annotations - -import re - -from .requoted import requoted - -_TOKENS = re.compile( - r'"(?:\\.|[^"\\])*"' r"|'(?:\\.|[^'\\])*'" r"|`(?:\\.|[^`\\])*`" r"|//[^\n]*" r"|/\*.*?\*/", - re.S, -) - - -def without_comments(source: str) -> str: - """`source` with comments removed and every string literal JSON-quoted.""" - - def keep(match: re.Match) -> str: - text = match.group(0) - return "" if text.startswith(("//", "/*")) else requoted(text) - - return _TOKENS.sub(keep, source) diff --git a/internals/checks/src/checks/platforms_mirror/without_comments_test.py b/internals/checks/src/checks/platforms_mirror/without_comments_test.py deleted file mode 100644 index dfe905fc8..000000000 --- a/internals/checks/src/checks/platforms_mirror/without_comments_test.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Colocated unit tests for the comment stripper (isolation -- pure text in, -text out; nothing here touches the repo's real platforms.ts). -""" - -from checks.platforms_mirror.without_comments import without_comments - - -def describe_without_comments(): - def it_strips_a_line_comment(): - assert without_comments("a // gone\nb") == "a \nb" - - def it_strips_a_line_comment_that_runs_to_end_of_file(): - assert without_comments("a // gone") == "a " - - def it_strips_a_block_comment(): - assert without_comments("a /* gone */ b") == "a b" - - def it_strips_a_block_comment_spanning_lines(): - assert without_comments("a /* one\ntwo */ b") == "a b" - - def it_keeps_a_double_quoted_string_verbatim(): - assert without_comments('x = "linux"') == 'x = "linux"' - - def it_requotes_a_single_quoted_string(): - assert without_comments("x = 'linux'") == 'x = "linux"' - - def it_requotes_a_template_literal(): - assert without_comments("x = `linux`") == 'x = "linux"' - - def it_does_not_read_a_line_comment_marker_inside_a_string(): - assert without_comments('x = "a // b"') == 'x = "a // b"' - - def it_does_not_read_a_block_comment_marker_inside_a_string(): - assert without_comments("x = 'a /* b'") == 'x = "a /* b"' - - def it_keeps_an_escaped_quote_inside_a_string(): - assert without_comments(r'x = "a\"b"') == r'x = "a\"b"' - - def it_leaves_source_with_neither_comment_nor_string_alone(): - assert without_comments("[1, 2]") == "[1, 2]" diff --git a/internals/checks/tests/e2e/platforms_mirror_test.py b/internals/checks/tests/e2e/platforms_mirror_test.py deleted file mode 100644 index e953e5430..000000000 --- a/internals/checks/tests/e2e/platforms_mirror_test.py +++ /dev/null @@ -1,98 +0,0 @@ -"""E2E test for `dirsql-checks platforms-mirror` through the real CLI. - -No mocking of any kind: spawns the packaged `dirsql-checks` console script as a -subprocess, once against real scratch files and once against the repo's own -`platforms.py` / `platforms.ts` through the command's default paths. -""" - -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path - -REPO = Path(__file__).resolve().parents[4] - -PYTHON_SOURCE = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: - node_platform: str - node_arch: str - slug: str - os: list[str] - cpu: list[str] -{extra} - -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu", ["linux"], ["x64"]{value}), -) -''' - -TYPESCRIPT_SOURCE = """\ -export const PLATFORMS: readonly Platform[] = [ - { - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "x64", - libName: "@dirsql/lib-linux-x64-gnu", - os: ["linux"], - cpu: ["x64"], - libc: ["glibc"], - }, -]; -""" - - -def _cli() -> str: - dirsql_checks = shutil.which("dirsql-checks") - assert dirsql_checks is not None, ( - "`dirsql-checks` console script not on PATH -- run " - "`uv run --project internals/checks pytest tests/e2e` " - "or `uv sync --project internals/checks`" - ) - return dirsql_checks - - -def _write(tmp_path, *, stray_field: bool): - python = tmp_path / "platforms.py" - python.write_text( - PYTHON_SOURCE.format( - extra=" exe: bool\n" if stray_field else "", - value=", False" if stray_field else "", - ) - ) - typescript = tmp_path / "platforms.ts" - typescript.write_text(TYPESCRIPT_SOURCE) - return ["--python", str(python), "--typescript", str(typescript)] - - -def _invoke(arguments, cwd=None): - return subprocess.run( - [_cli(), "platforms-mirror", *arguments], - capture_output=True, - text=True, - timeout=60, - cwd=cwd, - ) - - -def describe_dirsql_checks_platforms_mirror(): - def it_exits_zero_when_the_subset_agrees(tmp_path): - proc = _invoke(_write(tmp_path, stray_field=False)) - - assert proc.returncode == 0, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" - assert "ok platforms-mirror" in proc.stdout - - def it_exits_nonzero_and_names_the_unmirrored_field(tmp_path): - proc = _invoke(_write(tmp_path, stray_field=True)) - - assert proc.returncode == 1, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" - assert "Platform.exe" in proc.stdout - - def it_holds_the_repos_own_files_through_the_default_paths(): - proc = _invoke([], cwd=REPO) - - assert proc.returncode == 0, f"stdout={proc.stdout!r} stderr={proc.stderr!r}" diff --git a/internals/checks/tests/integration/platforms_mirror_test.py b/internals/checks/tests/integration/platforms_mirror_test.py deleted file mode 100644 index c680d275e..000000000 --- a/internals/checks/tests/integration/platforms_mirror_test.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Integration tests for the platforms-mirror check against real files. - -Exercises `gate.run` with its default collaborators -- a real `ast` parse of a -real `platforms.py` and a real parse of a real `platforms.ts` -- rather than the -packaged `dirsql-checks` CLI. - -The reproduction is #1004 exactly: `Platform.exe` sat on the Python dataclass for -the stated reason that it "mirrors packages/ts/src/platforms.ts", while the -TypeScript source of truth has never carried the field. A comment asserted the -correspondence and nothing checked it, which is the same mechanism behind #972 -and #996. The last case here is the one that matters day to day: the repo's own -two files, held to the invariant on every run. -""" - -from __future__ import annotations - -from pathlib import Path - -from checks.platforms_mirror.gate import run - -REPO = Path(__file__).resolve().parents[4] -PYTHON = REPO / "internals" / "distcheck" / "src" / "distcheck" / "node_flow" / "platforms.py" -TYPESCRIPT = REPO / "packages" / "ts" / "src" / "platforms.ts" - -ANNOTATIONS = { - "node_platform": "str", - "node_arch": "str", - "slug": "str", - "os": "list[str]", - "cpu": "list[str]", - "exe": "bool", -} - -SUBSET = ("node_platform", "node_arch", "slug", "os", "cpu") - -LINUX = { - "node_platform": "linux", - "node_arch": "x64", - "slug": "linux-x64-gnu", - "os": ["linux"], - "cpu": ["x64"], - "exe": False, -} -WINDOWS = { - "node_platform": "win32", - "node_arch": "x64", - "slug": "win32-x64-msvc", - "os": ["win32"], - "cpu": ["x64"], - "exe": True, -} - -PYTHON_SOURCE = '''\ -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Platform: -{fields} - - @property - def name(self) -> str: - return f"@dirsql/lib-{{self.slug}}" - - -PLATFORMS: tuple[Platform, ...] = ( -{rows} -) -''' - -TYPESCRIPT_SOURCE = """\ -export interface Platform {{ - triple: string; - nodePlatform: NodeJS.Platform; - nodeArch: NodeJS.Architecture; - libName: string; - os: string[]; - cpu: string[]; - libc?: string[]; -}} - -export const PLATFORMS: readonly Platform[] = [ -{rows} -]; -""" - - -def write(tmp_path, python_rows, typescript_rows, fields=SUBSET): - python = tmp_path / "platforms.py" - python.write_text( - PYTHON_SOURCE.format( - fields="\n".join(f" {name}: {ANNOTATIONS[name]}" for name in fields), - rows="\n".join( - f" Platform({', '.join(repr(row[name]) for name in fields)})," - for row in python_rows - ), - ) - ) - typescript = tmp_path / "platforms.ts" - typescript.write_text( - TYPESCRIPT_SOURCE.format( - rows="\n".join( - " {\n" - f' triple: "{row["node_arch"]}-{row["node_platform"]}",\n' - f' nodePlatform: "{row["node_platform"]}",\n' - f' nodeArch: "{row["node_arch"]}",\n' - f' libName: "{row.get("lib_name", "@dirsql/lib-" + row["slug"])}",\n' - f' os: {list(row["os"])!r},\n' - f' cpu: {list(row["cpu"])!r},\n' - " }," - for row in typescript_rows - ).replace("'", '"') - ) - ) - return str(python), str(typescript) - - -def describe_run_against_real_platform_files(): - def it_passes_when_the_declared_subset_agrees(tmp_path, capsys): - assert run(*write(tmp_path, [LINUX, WINDOWS], [LINUX, WINDOWS])) == 0 - assert "ok platforms-mirror" in capsys.readouterr().out - - def it_rejects_a_python_field_with_no_typescript_counterpart(tmp_path, capsys): - # #1004: `exe` justified by a mirror that never had it. - code = run(*write(tmp_path, [LINUX, WINDOWS], [LINUX, WINDOWS], fields=(*SUBSET, "exe"))) - - assert code == 1 - out = capsys.readouterr().out - assert "::error::Platform.exe" in out - assert "no counterpart in packages/ts/src/platforms.ts" in out - - def it_rejects_a_published_target_missing_from_the_python_rows(tmp_path, capsys): - # A new target half-added: published by the TS source of truth, invisible - # to the node distcheck flow. - code = run(*write(tmp_path, [LINUX], [LINUX, WINDOWS])) - - assert code == 1 - assert "::error::win32-x64 is published by platforms.ts" in capsys.readouterr().out - - def it_rejects_a_python_row_that_is_not_a_published_target(tmp_path, capsys): - code = run(*write(tmp_path, [LINUX, WINDOWS], [LINUX])) - - assert code == 1 - assert "::error::win32-x64 has a row in platforms.py" in capsys.readouterr().out - - def it_rejects_a_shared_field_whose_values_disagree(tmp_path, capsys): - drifted = {**WINDOWS, "cpu": ["arm64"]} - code = run(*write(tmp_path, [LINUX, drifted], [LINUX, WINDOWS])) - - assert code == 1 - out = capsys.readouterr().out - assert "::error::win32-x64: cpu is ['arm64'] in platforms.py" in out - assert "['x64'] in platforms.ts" in out - - def it_rejects_a_lib_name_that_does_not_carry_the_sub_package_prefix(tmp_path, capsys): - stray = {**WINDOWS, "lib_name": "@dirsql/win32-x64-msvc"} - code = run(*write(tmp_path, [LINUX, WINDOWS], [LINUX, stray])) - - assert code == 1 - assert "@dirsql/lib-" in capsys.readouterr().out - - -def describe_run_against_the_repos_own_files(): - def it_holds_the_committed_platform_tables_to_the_invariant(capsys): - assert run(str(PYTHON), str(TYPESCRIPT)) == 0, capsys.readouterr().out diff --git a/internals/distcheck/src/distcheck/node_flow/platforms.py b/internals/distcheck/src/distcheck/node_flow/platforms.py index 2f20d18e6..3044c80f9 100644 --- a/internals/distcheck/src/distcheck/node_flow/platforms.py +++ b/internals/distcheck/src/distcheck/node_flow/platforms.py @@ -1,17 +1,22 @@ """Host-platform detection for the node distcheck flow (#520). -A Python port of the host-row lookup in `packages/ts/src/platforms.ts`: enough -of each published target to reconstruct the host's `@dirsql/lib-` -sub-package (name, os/cpu constraints, addon filename) and locate its staged -addon. Pure -- callers pass the detected `sys.platform` / `platform.machine()`, -so the lookup is unit-testable. +Reads the published-target table from `packages/ts/src/platforms.json`, the one +declarative copy shared with the TypeScript SDK, and keeps the fields this flow +needs to reconstruct the host's `@dirsql/lib-` sub-package (name, os/cpu +constraints, addon filename) and locate its staged addon. The lookup is pure -- +callers pass the detected `sys.platform` / `platform.machine()`. """ from __future__ import annotations +import json from dataclasses import dataclass +from pathlib import Path from .arch import to_node_arch +_LIB_PREFIX = "@dirsql/lib-" +_TABLE = Path(__file__).parents[5] / "packages" / "ts" / "src" / "platforms.json" + @dataclass(frozen=True) class Platform: @@ -36,16 +41,15 @@ def addon_name(self) -> str: return f"dirsql.{self.slug}.node" -# The subset of PLATFORMS in packages/ts/src/platforms.ts this flow needs -- that -# file is the release source of truth for the published sub-packages, and its -# `triple` and `libc` reconstruct nothing here. `dirsql-checks platforms-mirror` -# holds the two tables to that subset, in both directions. -PLATFORMS: tuple[Platform, ...] = ( - Platform("linux", "x64", "linux-x64-gnu", ["linux"], ["x64"]), - Platform("linux", "arm64", "linux-arm64-gnu", ["linux"], ["arm64"]), - Platform("darwin", "x64", "darwin-x64", ["darwin"], ["x64"]), - Platform("darwin", "arm64", "darwin-arm64", ["darwin"], ["arm64"]), - Platform("win32", "x64", "win32-x64-msvc", ["win32"], ["x64"]), +PLATFORMS: tuple[Platform, ...] = tuple( + Platform( + row["nodePlatform"], + row["nodeArch"], + row["libName"].removeprefix(_LIB_PREFIX), + row["os"], + row["cpu"], + ) + for row in json.loads(_TABLE.read_text(encoding="utf-8")) ) _BY_KEY = {(p.node_platform, p.node_arch): p for p in PLATFORMS} @@ -56,7 +60,7 @@ def find_host_platform(node_platform: str, node_arch: str) -> Platform: if platform is None: raise ValueError( f"unsupported host {node_platform}-{node_arch}; " - "add a row to PLATFORMS in packages/ts/src/platforms.ts (and here)" + "add a row to packages/ts/src/platforms.json" ) return platform diff --git a/packages/ts/changelog.d/2026-09-05-platform-table-single-source.md b/packages/ts/changelog.d/2026-09-05-platform-table-single-source.md new file mode 100644 index 000000000..5be4c05ed --- /dev/null +++ b/packages/ts/changelog.d/2026-09-05-platform-table-single-source.md @@ -0,0 +1,6 @@ +**Changed** — the published-target table moved out of `src/platforms.ts` into +`src/platforms.json`, the one declarative copy of the data. `platforms.ts` types +and re-exports it; the node distcheck flow reads the same file instead of +hand-maintaining a Python subset, so the `platforms-mirror` check that policed +the two copies is gone. `PLATFORMS`, `libTriples()` and `librarySlug()` keep +their shapes, and the JSON ships in `dist/` alongside the compiled modules. diff --git a/packages/ts/package.json b/packages/ts/package.json index f6381bc77..ecf2a46e4 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -49,7 +49,12 @@ }, "tsc": { "command": "tsc -p tsconfig.build.json", - "files": ["src/**/*.ts", "tsconfig.json", "tsconfig.build.json"], + "files": [ + "src/**/*.ts", + "src/**/*.json", + "tsconfig.json", + "tsconfig.build.json" + ], "output": ["dist/**"] }, "stage:platform": { @@ -58,6 +63,7 @@ "files": [ "tools/stage-platform.ts", "src/platforms.ts", + "src/platforms.json", "napi/src/**/*.rs", "napi/build.rs", "napi/Cargo.toml", @@ -77,6 +83,7 @@ "napi/Cargo.toml", "../rust/src/**/*.rs", "src/**/*.ts", + "src/**/*.json", "tsconfig.json", "tsconfig.build.json" ], @@ -87,6 +94,7 @@ "dependencies": ["napi:build", "tsc"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/**/*.ts", "vitest.config.ts" @@ -96,13 +104,14 @@ "test:unit": { "command": "vitest run --dir src", "dependencies": ["napi:build"], - "files": ["src/**/*.ts", "vitest.config.ts"], + "files": ["src/**/*.ts", "src/**/*.json", "vitest.config.ts"], "output": [] }, "test:integration": { "command": "vitest run --dir tests/integration/hermetic --passWithNoTests", "files": [ "src/**/*.ts", + "src/**/*.json", "tests/integration/hermetic/**/*.ts", "vitest.config.ts" ], @@ -113,6 +122,7 @@ "dependencies": ["napi:build", "tsc"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/integration/binding/**/*.ts", "tests/exists.ts", @@ -125,6 +135,7 @@ "dependencies": ["build"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/e2e/**/*.ts", "vitest.config.ts", @@ -138,6 +149,7 @@ "dependencies": ["napi:build"], "files": [ "src/**/*.ts", + "src/**/*.json", "tools/**/*.ts", "tests/integration/**/*.ts", "tests/exists.ts", diff --git a/packages/ts/src/platforms.json b/packages/ts/src/platforms.json new file mode 100644 index 000000000..c19b559a0 --- /dev/null +++ b/packages/ts/src/platforms.json @@ -0,0 +1,44 @@ +[ + { + "triple": "x86_64-unknown-linux-gnu", + "nodePlatform": "linux", + "nodeArch": "x64", + "libName": "@dirsql/lib-linux-x64-gnu", + "os": ["linux"], + "cpu": ["x64"], + "libc": ["glibc"] + }, + { + "triple": "aarch64-unknown-linux-gnu", + "nodePlatform": "linux", + "nodeArch": "arm64", + "libName": "@dirsql/lib-linux-arm64-gnu", + "os": ["linux"], + "cpu": ["arm64"], + "libc": ["glibc"] + }, + { + "triple": "x86_64-apple-darwin", + "nodePlatform": "darwin", + "nodeArch": "x64", + "libName": "@dirsql/lib-darwin-x64", + "os": ["darwin"], + "cpu": ["x64"] + }, + { + "triple": "aarch64-apple-darwin", + "nodePlatform": "darwin", + "nodeArch": "arm64", + "libName": "@dirsql/lib-darwin-arm64", + "os": ["darwin"], + "cpu": ["arm64"] + }, + { + "triple": "x86_64-pc-windows-msvc", + "nodePlatform": "win32", + "nodeArch": "x64", + "libName": "@dirsql/lib-win32-x64-msvc", + "os": ["win32"], + "cpu": ["x64"] + } +] diff --git a/packages/ts/src/platforms.ts b/packages/ts/src/platforms.ts index e09e2d18b..c78a8f1fb 100644 --- a/packages/ts/src/platforms.ts +++ b/packages/ts/src/platforms.ts @@ -1,7 +1,11 @@ -// Single source of truth for the target platforms `dirsql` publishes. +// The target platforms `dirsql` publishes. The rows live in `platforms.json`, +// the one declarative copy of the table: this module types it, and the node +// distcheck flow (`internals/distcheck/.../node_flow/platforms.py`) reads the +// same file. The JSON sits under `src/` so `tsc` emits it into `dist/` and it +// ships with the package -- `loadNativeCore()` needs the map at runtime. // // Every target triple generates ONE npm sub-package: `@dirsql/lib-`, -// holding the napi-rs `.node` addon. It backs both layers — the TypeScript +// holding the napi-rs `.node` addon. It backs both layers -- the TypeScript // SDK loads it via `loadNativeCore()`, and since #739 the `dirsql` CLI runs // in-process through its `runCli` export rather than spawning a binary. The // second family, `@dirsql/cli-`, shipped a redundant copy of the same @@ -13,6 +17,8 @@ // `libTriples()` returns a `${process.platform}-${process.arch}` → // sub-package-name map. +import rows from "./platforms.json" with { type: "json" }; + export interface Platform { /** Rust target triple — the name cargo-dist uses for archives. */ triple: string; @@ -30,50 +36,10 @@ export interface Platform { libc?: string[]; } -export const PLATFORMS: readonly Platform[] = [ - { - triple: "x86_64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "x64", - libName: "@dirsql/lib-linux-x64-gnu", - os: ["linux"], - cpu: ["x64"], - libc: ["glibc"], - }, - { - triple: "aarch64-unknown-linux-gnu", - nodePlatform: "linux", - nodeArch: "arm64", - libName: "@dirsql/lib-linux-arm64-gnu", - os: ["linux"], - cpu: ["arm64"], - libc: ["glibc"], - }, - { - triple: "x86_64-apple-darwin", - nodePlatform: "darwin", - nodeArch: "x64", - libName: "@dirsql/lib-darwin-x64", - os: ["darwin"], - cpu: ["x64"], - }, - { - triple: "aarch64-apple-darwin", - nodePlatform: "darwin", - nodeArch: "arm64", - libName: "@dirsql/lib-darwin-arm64", - os: ["darwin"], - cpu: ["arm64"], - }, - { - triple: "x86_64-pc-windows-msvc", - nodePlatform: "win32", - nodeArch: "x64", - libName: "@dirsql/lib-win32-x64-msvc", - os: ["win32"], - cpu: ["x64"], - }, -]; +// JSON widens `nodePlatform` / `nodeArch` to `string`, which no data file can +// carry as `NodeJS.Platform` / `NodeJS.Architecture`. The cast is the only +// place the two vocabularies meet. +export const PLATFORMS = rows as readonly Platform[]; /** Node `${platform}-${arch}` → `@dirsql/lib-*` napi sub-package name. */ export function libTriples(): Record { diff --git a/packages/ts/tsconfig.json b/packages/ts/tsconfig.json index 4688441f5..badbf2328 100644 --- a/packages/ts/tsconfig.json +++ b/packages/ts/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", + "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true, From 98aa58f2bbd95a4bd448c4274dc3b798e344ba51 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:58:40 -0400 Subject: [PATCH 2/4] test: pin the platform table's node vocabulary and slug set The TypeScript view of the table is now a cast over platforms.json, so no type stops a hand-edited row carrying a platform/arch node never reports; the Python view derives `slug` from `libName` rather than carrying it. Two new assertions pin both, one per side. No existing assertion changed. --- .../src/distcheck/node_flow/platforms_test.py | 12 ++++ packages/ts/src/platforms.test.ts | 15 ++++ packages/ts/stryker-setup-10.js | 71 +++++++++++++++++++ packages/ts/tools/stage-platform.ts | 2 +- 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/ts/stryker-setup-10.js diff --git a/internals/distcheck/src/distcheck/node_flow/platforms_test.py b/internals/distcheck/src/distcheck/node_flow/platforms_test.py index ac76e8bb6..2fbf5806a 100644 --- a/internals/distcheck/src/distcheck/node_flow/platforms_test.py +++ b/internals/distcheck/src/distcheck/node_flow/platforms_test.py @@ -43,3 +43,15 @@ def test_platform_is_immutable(): p = find_host_platform("linux", "x64") with pytest.raises(AttributeError): p.slug = "mutated" + + +def test_platforms_covers_every_published_target(): + # PLATFORMS is read from packages/ts/src/platforms.json and `slug` is the + # one field derived rather than carried; pin the whole set. + assert sorted(p.slug for p in PLATFORMS) == [ + "darwin-arm64", + "darwin-x64", + "linux-arm64-gnu", + "linux-x64-gnu", + "win32-x64-msvc", + ] diff --git a/packages/ts/src/platforms.test.ts b/packages/ts/src/platforms.test.ts index 2c20fb0ae..07c107b79 100644 --- a/packages/ts/src/platforms.test.ts +++ b/packages/ts/src/platforms.test.ts @@ -44,6 +44,21 @@ describe("PLATFORMS", () => { }); }); + describe("the node vocabulary the JSON cast cannot type", () => { + it("pairs a real `process.platform` with a real `process.arch` on every row", () => { + // `PLATFORMS` is a cast over platforms.json, so nothing at the type + // level stops a hand-edited row carrying a value node never reports. + const keys = PLATFORMS.map((p) => `${p.nodePlatform}-${p.nodeArch}`); + expect(keys.sort()).toEqual([ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "win32-x64", + ]); + }); + }); + describe("libTriples()", () => { it("maps every `${platform}-${arch}` key to its lib-* sub-package", () => { const map = libTriples(); diff --git a/packages/ts/stryker-setup-10.js b/packages/ts/stryker-setup-10.js new file mode 100644 index 000000000..aecfacbe4 --- /dev/null +++ b/packages/ts/stryker-setup-10.js @@ -0,0 +1,71 @@ +import { beforeEach, afterAll, beforeAll, afterEach, inject, } from 'vitest'; +// This file is copied to the sandbox dir, don't import anything local! +// See https://github.com/stryker-mutator/stryker-js/issues/5305 +const globalNamespace = inject('globalNamespace'); +const mutantActivation = inject('mutantActivation'); +const mode = inject('mode'); +const ns = globalThis[globalNamespace] || (globalThis[globalNamespace] = {}); +ns.hitLimit = inject('hitLimit'); +const isGreaterThanVitest4Point1 = inject('isGreaterThanVitest4Point1'); +if (mode === 'mutant') { + beforeAll(() => { + ns.hitCount = 0; + }); + if (mutantActivation === 'static') { + ns.activeMutant = inject('activeMutant'); + } + else { + beforeAll(() => { + ns.activeMutant = inject('activeMutant'); + }); + } + if (isGreaterThanVitest4Point1) { + // Vitest's hooks API requires this empty destructure to allow access to suite.meta + // eslint-disable-next-line no-empty-pattern + afterAll(({}, suite) => { + suite.meta.hitCount = ns.hitCount; + }); + } + else { + // @ts-expect-error This was changed in Vitest v4.1 + afterAll(({ meta }) => { + meta.hitCount = ns.hitCount; + }); + } +} +else { + ns.activeMutant = undefined; + beforeEach(({ task }) => { + ns.currentTestId = toRawTestId(task); + }); + afterEach(() => { + ns.currentTestId = undefined; + }); + if (isGreaterThanVitest4Point1) { + // Vitest's hooks API requires this empty destructure to allow access to suite.meta + // eslint-disable-next-line no-empty-pattern + afterAll(({}, suite) => { + suite.meta.mutantCoverage = ns.mutantCoverage; + }); + } + else { + // @ts-expect-error This was changed in Vitest v4.1 + afterAll(({ meta }) => { + meta.mutantCoverage = ns.mutantCoverage; + }); + } +} +// Stryker disable all: this file is copied to the sandbox dir +function collectTestName({ name, suite, }) { + const nameParts = [name]; + let currentSuite = suite; + while (currentSuite) { + nameParts.unshift(currentSuite.name); + currentSuite = currentSuite.suite; + } + return nameParts.join(' ').trim(); +} +function toRawTestId(test) { + return `${test.file?.filepath ?? 'unknown.js'}#${collectTestName(test)}`; +} +//# sourceMappingURL=stryker-setup.js.map \ No newline at end of file diff --git a/packages/ts/tools/stage-platform.ts b/packages/ts/tools/stage-platform.ts index 1aac15a0c..aed9275b7 100644 --- a/packages/ts/tools/stage-platform.ts +++ b/packages/ts/tools/stage-platform.ts @@ -37,7 +37,7 @@ export function findHostPlatform( ); if (!p) { throw new Error( - `unsupported host ${key}; add a row to PLATFORMS in src/platforms.ts`, + `unsupported host ${key}; add a row to src/platforms.json`, ); } return p; From c92fd606e9cbc936828378f5481e0c0dbdda98c0 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:00:59 -0400 Subject: [PATCH 3/4] e2e attestation for claude/1074-platform-table-single-source --- .../claude-1074-platform-table-single-source.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json diff --git a/packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json b/packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json new file mode 100644 index 000000000..2ca3014eb --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1074-platform-table-single-source.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1788609659, + "exit_code": 0, + "commit": "98aa58f2bbd95a4bd448c4274dc3b798e344ba51", + "branch": "claude/1074-platform-table-single-source" +} From 6037ce4db8a210f375557feb78034fe83abeecf5 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:01:09 -0400 Subject: [PATCH 4/4] e2e attestation for claude/1074-platform-table-single-source --- .../claude-1074-platform-table-single-source.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json diff --git a/internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json b/internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json new file mode 100644 index 000000000..b0b55bccc --- /dev/null +++ b/internals/checks/e2e-attestations/claude-1074-platform-table-single-source.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e -x -q", + "ran_at": 1788609669, + "exit_code": 0, + "commit": "c92fd606e9cbc936828378f5481e0c0dbdda98c0", + "branch": "claude/1074-platform-table-single-source" +}