diff --git a/.github/pyrightconfig.json b/.github/pyrightconfig.json
new file mode 100644
index 0000000..6ceb2c0
--- /dev/null
+++ b/.github/pyrightconfig.json
@@ -0,0 +1,15 @@
+{
+ "typeCheckingMode": "strict",
+ "pythonVersion": "3.13",
+ "include": [
+ "scripts/*.py"
+ ],
+ "reportMissingTypeStubs": "error",
+ "reportUnnecessaryTypeIgnoreComment": "error",
+ "reportImplicitOverride": "error",
+ "exclude": [
+ "**/__pycache__"
+ ],
+ "venvPath": "..",
+ "venv": ".venv"
+}
diff --git a/.github/scripts/check-codeql-sarif.py b/.github/scripts/check-codeql-sarif.py
index 8705490..7177466 100644
--- a/.github/scripts/check-codeql-sarif.py
+++ b/.github/scripts/check-codeql-sarif.py
@@ -2,55 +2,107 @@
# SPDX-License-Identifier: Apache-2.0
"""Reject missing/malformed CodeQL evidence before enforcing the severity gate."""
+
import json
import math
import os
from pathlib import Path
+from typing import TypedDict
+
+RuleProperties = TypedDict("RuleProperties", {"security-severity": str | float | None}, total=False)
-def check(directory, language, threshold):
- if not math.isfinite(threshold) or not 0 <= threshold <= 10:
- raise ValueError("Invalid security severity threshold")
- canonical = {"java-kotlin": "java", "javascript-typescript": "javascript", "c-cpp": "cpp"}.get(language, language)
- files = sorted(Path(directory).glob("*.sarif"))
- if not canonical or not any(path.name == canonical + ".sarif" for path in files):
- raise ValueError("Missing expected CodeQL SARIF report for " + language)
- blocking = []
- for path in files:
- report = json.loads(path.read_text(encoding="utf-8"))
- runs = report.get("runs")
- if report.get("version") != "2.1.0" or not isinstance(runs, list) or not runs:
- raise ValueError("Invalid or empty SARIF runs: " + path.name)
- for run in runs:
- driver = run.get("tool", {}).get("driver", {})
- results = run.get("results")
- if driver.get("name") != "CodeQL" or not isinstance(driver.get("rules"), list) or not isinstance(results, list):
- raise ValueError("Missing CodeQL tool/rules/results evidence: " + path.name)
- rules = {}
- for component in [driver, *run.get("tool", {}).get("extensions", [])]:
- for rule in component.get("rules", []):
- rules[rule["id"]] = rule
- if not rules:
- raise ValueError("CodeQL report has no analyzed rule metadata: " + path.name)
- for invocation in run.get("invocations", []):
- if invocation.get("executionSuccessful") is False:
- raise ValueError("CodeQL invocation failed: " + path.name)
- for result in results:
- rule_id = result.get("ruleId") or result.get("rule", {}).get("id")
- if rule_id not in rules:
- raise ValueError("Result references missing rule metadata: " + path.name)
- if result.get("suppressions"):
- continue
- severity = float(rules[rule_id].get("properties", {}).get("security-severity", 0) or 0)
- if not math.isfinite(severity) or not 0 <= severity <= 10:
- raise ValueError("Invalid rule severity: " + rule_id)
- if severity >= threshold:
- blocking.append((severity, rule_id))
- for severity, rule_id in sorted(blocking, reverse=True):
- print(f"::error title=CodeQL security finding::{rule_id} ({severity})")
- print(f"Validated {len(files)} SARIF report(s); {len(blocking)} blocking result(s)")
- return bool(blocking)
+class Rule(TypedDict):
+ """Keep the rule identity and optional severity metadata used by the gate."""
+
+ id: str
+ properties: RuleProperties
+
+
+class Component(TypedDict, total=False):
+ """Describe CodeQL driver or extension rule metadata."""
+
+ name: str
+ rules: list[Rule]
+
+
+class Tool(TypedDict, total=False):
+ """Group the driver and optional extension rule catalogs."""
+
+ driver: Component
+ extensions: list[Component]
+
+
+class Result(TypedDict, total=False):
+ """Describe only the result fields consumed by the severity gate."""
+
+ ruleId: str
+ rule: dict[str, str]
+ suppressions: list[object]
+
+
+class Run(TypedDict, total=False):
+ """Retain invocation success and findings for one analyzed language."""
+
+ tool: Tool
+ results: list[Result]
+ invocations: list[dict[str, bool]]
+
+
+class Report(TypedDict, total=False):
+ """Represent SARIF fields while retaining the explicit runtime validation."""
+
+ version: str
+ runs: list[Run]
+
+
+MAX_SEVERITY = 10
+
+
+def check(directory: str | Path, language: str, threshold: float) -> bool: # noqa: PLR0912 -- Keep the ordered evidence checks together; none may be skipped.
+ """Validate complete CodeQL evidence and report findings at the threshold."""
+ if not math.isfinite(threshold) or not 0 <= threshold <= MAX_SEVERITY:
+ raise ValueError("Invalid security severity threshold")
+ canonical = {"java-kotlin": "java", "javascript-typescript": "javascript", "c-cpp": "cpp"}.get(language, language)
+ files = sorted(Path(directory).glob("*.sarif"))
+ if not canonical or not any(path.name == canonical + ".sarif" for path in files):
+ raise ValueError("Missing expected CodeQL SARIF report for " + language)
+ blocking: list[tuple[float, str]] = []
+ for path in files:
+ report: Report = json.loads(path.read_text(encoding="utf-8"))
+ runs = report.get("runs")
+ if report.get("version") != "2.1.0" or not isinstance(runs, list) or not runs:
+ raise ValueError("Invalid or empty SARIF runs: " + path.name)
+ for run in runs:
+ driver = run.get("tool", {}).get("driver", {})
+ results = run.get("results")
+ if driver.get("name") != "CodeQL" or not isinstance(driver.get("rules"), list) or not isinstance(results, list):
+ raise ValueError("Missing CodeQL tool/rules/results evidence: " + path.name)
+ rules: dict[str, Rule] = {}
+ for component in [driver, *run.get("tool", {}).get("extensions", [])]:
+ for rule in component.get("rules", []):
+ rules[rule["id"]] = rule
+ if not rules:
+ raise ValueError("CodeQL report has no analyzed rule metadata: " + path.name)
+ for invocation in run.get("invocations", []):
+ if invocation.get("executionSuccessful") is False:
+ raise ValueError("CodeQL invocation failed: " + path.name)
+ for result in results:
+ rule_id = result.get("ruleId") or result.get("rule", {}).get("id")
+ if rule_id not in rules:
+ raise ValueError("Result references missing rule metadata: " + path.name)
+ if result.get("suppressions"):
+ continue
+ severity = float(rules[rule_id].get("properties", {}).get("security-severity", 0) or 0)
+ if not math.isfinite(severity) or not 0 <= severity <= MAX_SEVERITY:
+ raise ValueError("Invalid rule severity: " + rule_id)
+ if severity >= threshold:
+ blocking.append((severity, rule_id))
+ for severity, rule_id in sorted(blocking, reverse=True):
+ print(f"::error title=CodeQL security finding::{rule_id} ({severity})")
+ print(f"Validated {len(files)} SARIF report(s); {len(blocking)} blocking result(s)")
+ return bool(blocking)
if __name__ == "__main__":
- raise SystemExit(1 if check(os.environ["SARIF_DIR"], os.environ["EXPECTED_LANGUAGE"], float(os.environ["THRESHOLD"])) else 0)
+ raise SystemExit(1 if check(os.environ["SARIF_DIR"], os.environ["EXPECTED_LANGUAGE"], float(os.environ["THRESHOLD"])) else 0)
diff --git a/.github/scripts/check-script-quality.py b/.github/scripts/check-script-quality.py
new file mode 100644
index 0000000..fa3b4df
--- /dev/null
+++ b/.github/scripts/check-script-quality.py
@@ -0,0 +1,144 @@
+# Copyright 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+"""Fail closed unless every tracked owned script is covered by, and really analyzed by, the quality tools."""
+
+import hashlib
+import json
+import re
+import shutil
+import subprocess
+import sys
+from pathlib import Path, PurePosixPath
+from typing import Any
+
+PYTHON_SUFFIXES = (".py", ".pyi", ".pyw")
+SCRIPT_PATTERN = ".github/scripts/*.py"
+PYRIGHT_CONFIG = ".github"
+JAVASCRIPT_SUFFIXES = (".js", ".cjs", ".mjs", ".jsx", ".ts", ".mts", ".cts", ".tsx")
+# EX-0015: the only npm package whose license classification the PR gate may exempt (the matcher ignores versions, so this guard pins it).
+URI_JS_PURL = "pkg:npm/uri-js"
+URI_JS_VERSION = "4.4.1"
+URI_JS_INTEGRITY = "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="
+URI_JS_LICENSE_SHA256 = "0af366eff4c01ec147c9c61ea9e8ffad64a4294754c9d79355f3fd1b97cb2fb9"
+PR_GATES = ".github/workflows/pr-gates.yml"
+# Microsoft's official npm Pyright, restored from package-lock.json by `npm ci --ignore-scripts`.
+PYRIGHT_ENTRY = Path(__file__).resolve().parents[2] / "node_modules" / "pyright" / "index.js"
+
+
+def tracked_files(root: Path) -> list[str]:
+ """List tracked and not-yet-ignored new paths so no source file can silently bypass the gates."""
+ # Fixed Git inventory operation; the root is an argument, never shell input.
+ output = subprocess.run(["git", "-C", str(root), "ls-files", "-z", "--cached", "--others", "--exclude-standard"], check=True, capture_output=True).stdout.decode("utf-8") # noqa: S603, S607 -- Resolve Git from the developer/CI PATH; no shell or user-supplied command.
+ return [name for name in output.split("\u0000") if name]
+
+
+def python_scope(tracked: list[str]) -> list[str]:
+ """Return the tracked Python sources, rejecting any outside the one checked script root."""
+ candidates = sorted(name for name in tracked if name.endswith(PYTHON_SUFFIXES))
+ outside = [name for name in candidates if not PurePosixPath(name).full_match(SCRIPT_PATTERN)]
+ if outside:
+ raise ValueError("Tracked Python outside the checked script root: " + ", ".join(outside))
+ if not candidates:
+ raise ValueError("No owned Python scripts selected")
+ return candidates
+
+
+def javascript_scope(root: Path, tracked: list[str]) -> list[str]:
+ """Return tracked JavaScript/TypeScript sources, rejecting any outside the tsconfig include list."""
+ candidates = sorted(name for name in tracked if name.endswith(JAVASCRIPT_SUFFIXES))
+ tsconfig = root / "tsconfig.json"
+ if not tsconfig.is_file():
+ if candidates:
+ raise ValueError("Tracked JavaScript/TypeScript without a tsconfig.json: " + ", ".join(candidates))
+ return []
+ includes: list[str] = json.loads(tsconfig.read_text(encoding="utf-8"))["include"]
+ outside = [name for name in candidates if not any(PurePosixPath(name).full_match(pattern) for pattern in includes)]
+ if outside:
+ raise ValueError("Tracked JavaScript/TypeScript outside the tsconfig include list: " + ", ".join(outside))
+ if not candidates:
+ raise ValueError("No owned JavaScript sources selected")
+ return candidates
+
+
+def run_pyright(root: Path, config: str) -> dict[str, Any]:
+ """Run the locked npm Pyright through Node with JSON output and return its full report."""
+ node = shutil.which("node")
+ if node is None:
+ raise ValueError("Node.js is required to run the locked Pyright")
+ if not PYRIGHT_ENTRY.is_file():
+ raise ValueError(f"Locked Pyright is missing; run npm ci --ignore-scripts ({PYRIGHT_ENTRY})")
+ # Resolved Node and locked entry script; the config is an argument, never shell input.
+ completed = subprocess.run([node, str(PYRIGHT_ENTRY), "-p", config, "--outputjson"], cwd=root, check=False, capture_output=True, text=True, encoding="utf-8") # noqa: S603 -- Resolved Node and locked entry script; no shell.
+ try:
+ report: dict[str, Any] = json.loads(completed.stdout[completed.stdout.index("{") :])
+ int(report["summary"]["filesAnalyzed"]) # Fail here when the summary shape is missing.
+ report["exitCode"] = completed.returncode
+ except (ValueError, KeyError) as error:
+ raise ValueError(f"Pyright produced no JSON summary (exit {completed.returncode}): {completed.stdout}{completed.stderr}") from error
+ return report
+
+
+def check_pyright_report(report: dict[str, Any], expected: int) -> None:
+ """Require the analyzed-file count to match the selected sources, with no errors or warnings."""
+ summary = report["summary"]
+ analyzed = summary["filesAnalyzed"]
+ if analyzed == 0 or analyzed != expected:
+ raise ValueError(f"Pyright analyzed {analyzed} files, expected {expected}")
+ if summary["errorCount"] or summary["warningCount"]:
+ details = [f"{item['file']}: {item['message']}" for item in report["generalDiagnostics"]]
+ raise ValueError("Pyright reported findings:\n" + "\n".join(details))
+ if report["exitCode"] != 0:
+ raise ValueError(f"Pyright exited {report['exitCode']} despite a clean summary")
+
+
+def run_ruff(root: Path, arguments: list[str], sources: list[str]) -> None:
+ """Run Ruff on the explicit source list so no selected file can be skipped."""
+ # Fixed interpreter module invocation; paths are arguments, never shell input.
+ if subprocess.run([sys.executable, "-m", "ruff", *arguments, "--no-cache", *sources], cwd=root, check=False).returncode != 0: # noqa: S603 -- Fixed interpreter and module; no shell.
+ raise ValueError("Ruff " + " ".join(arguments) + " failed")
+
+
+def check_uri_js_exception(root: Path) -> None:
+ """Allow the EX-0015 license exception only for the reviewed, unmodified, development-only uri-js."""
+ packages: dict[str, dict[str, Any]] = json.loads((root / "package-lock.json").read_text(encoding="utf-8"))["packages"]
+ copies = [entry for path, entry in packages.items() if path.rsplit("node_modules/", 1)[-1] == "uri-js"]
+ if len(copies) != 1:
+ raise ValueError(f"EX-0015 covers exactly one locked uri-js copy, found {len(copies)}")
+ if copies[0].get("version") != URI_JS_VERSION or copies[0].get("integrity") != URI_JS_INTEGRITY or copies[0].get("dev") is not True:
+ raise ValueError(f"EX-0015 covers only development-only uri-js {URI_JS_VERSION} with the reviewed integrity")
+ root_entry = packages.get("", {})
+ if "uri-js" in {*root_entry.get("dependencies", {}), *root_entry.get("devDependencies", {})}:
+ raise ValueError("EX-0015 covers uri-js only as a transitive tool dependency, not a direct one")
+ consumers = [path for path, entry in packages.items() if "uri-js" in entry.get("dependencies", {}) and entry.get("dev") is not True]
+ if consumers:
+ raise ValueError("uri-js is required by a non-development package: " + ", ".join(consumers))
+ digest = hashlib.sha256((root / "node_modules" / "uri-js" / "LICENSE").read_bytes()).hexdigest()
+ if digest != URI_JS_LICENSE_SHA256:
+ raise ValueError(f"Installed uri-js LICENSE bytes differ from the reviewed EX-0015 copy ({digest})")
+ caller = [line for line in (root / PR_GATES).read_text(encoding="utf-8").splitlines() if not line.lstrip().startswith("#")]
+ listed = re.findall(r"pkg:npm/[^\s,]+", "\n".join(caller))
+ if listed != [URI_JS_PURL]:
+ raise ValueError(f"EX-0015 permits only {URI_JS_PURL} in allow-dependencies-licenses, found {listed}")
+
+
+def main() -> None:
+ """Verify scope, then Ruff format/lint and strict Pyright with a positive analyzed count."""
+ root = Path(__file__).resolve().parents[2]
+ tracked = tracked_files(root)
+ sources = python_scope(tracked)
+ javascript = javascript_scope(root, tracked)
+ run_ruff(root, ["format", "--check"], sources)
+ run_ruff(root, ["check"], sources)
+ report = run_pyright(root, PYRIGHT_CONFIG)
+ check_pyright_report(report, len(sources))
+ check_uri_js_exception(root)
+ print(f"Script quality verified: {len(sources)} Python scripts analyzed by Ruff and Pyright, {len(javascript)} JavaScript files in tsconfig scope")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except (ValueError, OSError, subprocess.CalledProcessError) as error:
+ print(str(error), file=sys.stderr)
+ sys.exit(1)
diff --git a/.github/scripts/check-source-headers.py b/.github/scripts/check-source-headers.py
index 2392c7e..e98fd8f 100644
--- a/.github/scripts/check-source-headers.py
+++ b/.github/scripts/check-source-headers.py
@@ -19,14 +19,14 @@ def selected(name: str, policy: dict[str, Any]) -> bool:
return any(fnmatch.fnmatchcase(name, pattern) for pattern in policy["includes"])
-def leading_comments(text: str, style: str) -> str:
+def leading_comments(text: str, style: str) -> str: # noqa: PLR0912 -- Each recognized leading-comment syntax has a distinct fail-closed branch.
"""Read comments only, preserving required interpreter/XML directive ordering."""
text = text.removeprefix("\ufeff")
if text.startswith("", text)
if style != "xml" or declaration is None:
raise ValueError("Invalid XML declaration")
- text = text[declaration.end():]
+ text = text[declaration.end() :]
elif text.startswith("#!"):
if style not in ("hash", "powershell", "slash"):
raise ValueError("Unexpected interpreter directive")
@@ -35,7 +35,7 @@ def leading_comments(text: str, style: str) -> str:
if style != "powershell":
raise ValueError("Unexpected PowerShell directive")
text = text.partition("\n")[2]
- comments = []
+ comments: list[str] = []
while text.strip():
text = text.lstrip()
if text.startswith(" str:
elif style == "slash" and text.startswith("//"):
line, _, text = text.partition("\n")
comments.append(line[2:])
- elif ((style == "slash" and text.startswith("/*"))
- or (style == "xml" and text.startswith(""), "powershell": ("<#", "#>")}[style]
- body, separator, rest = text[len(opening):].partition(closing)
+ body, separator, rest = text[len(opening) :].partition(closing)
if not separator:
raise ValueError("Unterminated leading comment")
comments.append(body)
@@ -72,19 +70,22 @@ def check_notice(name: str, text: str, policy: dict[str, Any]) -> None:
approved = [item for item in policy["notices"] if any(fnmatch.fnmatchcase(name, pattern) for pattern in item["paths"])]
for notice in approved:
holder = re.escape(notice["holder"])
- copyright_pattern = rf"(?m)^Copyright\s+(?:\([cC]\)\s*)?\d{{4}}(?:[-–]\d{{4}})?\.?\s+{holder}(?:\s+\(KofTwentyTwo\))?\.?(?:\s+Licensed under the MIT License \(see LICENSE\)\.)?$"
+ copyright_pattern = rf"(?m)^Copyright\s+(?:\([cC]\)\s*)?\d{{4}}(?:[-\u2013]\d{{4}})?\.?\s+{holder}(?:\s+\(KofTwentyTwo\))?\.?(?:\s+Licensed under the MIT License \(see LICENSE\)\.)?$"
if re.search(copyright_pattern, comments) is None:
continue
spdx = re.findall(r"(?m)^SPDX-License-Identifier:\s*(\S+)\s*$", comments)
if spdx and any(value != notice["license"] for value in spdx):
continue
- apache = all(phrase in comments for phrase in (
- "Licensed under the Apache License, Version 2.0",
- "you may not use this file except in compliance with the License.",
- "https://www.apache.org/licenses/LICENSE-2.0",
- "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND",
- "limitations under the License.",
- ))
+ apache = all(
+ phrase in comments
+ for phrase in (
+ "Licensed under the Apache License, Version 2.0",
+ "you may not use this file except in compliance with the License.",
+ "https://www.apache.org/licenses/LICENSE-2.0",
+ "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND",
+ "limitations under the License.",
+ )
+ )
if suffix == ".java" and not apache:
continue
if notice["license"] in spdx or (notice["license"] == "Apache-2.0" and apache):
@@ -94,8 +95,9 @@ def check_notice(name: str, text: str, policy: dict[str, Any]) -> None:
def check_repository(root: Path, policy: dict[str, Any]) -> int:
"""Check tracked sources without scanning build outputs or executing source."""
- tracked = subprocess.run(["git", "-C", str(root), "ls-files", "-z"], check=True, capture_output=True).stdout.decode("utf-8").split("\u0000")
- failures = []
+ # Fixed Git inventory operation; paths are arguments, never shell input.
+ tracked = subprocess.run(["git", "-C", str(root), "ls-files", "-z"], check=True, capture_output=True).stdout.decode("utf-8").split("\u0000") # noqa: S603, S607 -- Resolve Git from the developer/CI PATH; no shell or user-supplied command.
+ failures: list[str] = []
count = 0
for name in tracked:
if not name or not selected(name, policy):
diff --git a/.github/scripts/test-scan-gates.py b/.github/scripts/test-scan-gates.py
index c8494eb..2b173dd 100644
--- a/.github/scripts/test-scan-gates.py
+++ b/.github/scripts/test-scan-gates.py
@@ -2,91 +2,106 @@
# SPDX-License-Identifier: Apache-2.0
"""Exercise the exact CI guards using synthetic evidence only."""
+
import contextlib
import copy
-import io
import importlib.util
+import io
import json
-from pathlib import Path
import sys
import tempfile
import unittest
-
+from pathlib import Path
+from types import ModuleType
+from typing import Any, override
sys.dont_write_bytecode = True
-def load(name):
- spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + ".py"))
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
+def load(name: str) -> ModuleType:
+ """Load the real sibling guard for synthetic evidence tests."""
+ spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + ".py"))
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
codeql = load("check-codeql-sarif")
class SarifTests(unittest.TestCase):
- def setUp(self):
- self.temporary = tempfile.TemporaryDirectory()
- self.addCleanup(self.temporary.cleanup)
- self.directory = Path(self.temporary.name)
- self.report = {"version": "2.1.0", "runs": [{"tool": {"driver": {"name": "CodeQL", "rules": [{"id": "synthetic", "properties": {"security-severity": "8.0"}}]}}, "results": []}]}
+ """Reject incomplete SARIF evidence and preserve severity enforcement."""
- def save(self, report):
- (self.directory / "java.sarif").write_text(json.dumps(report), encoding="utf-8")
+ @override
+ def setUp(self) -> None:
+ """Create isolated synthetic evidence with cleanup after each test."""
+ self.temporary = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temporary.cleanup)
+ self.directory = Path(self.temporary.name)
+ self.report: dict[str, Any] = {"version": "2.1.0", "runs": [{"tool": {"driver": {"name": "CodeQL", "rules": [{"id": "synthetic", "properties": {"security-severity": "8.0"}}]}}, "results": []}]}
- def test_clean_report_with_zero_findings_passes(self):
- self.save(self.report)
- self.assertFalse(codeql.check(self.directory, "java-kotlin", 7))
+ def save(self, report: object) -> None:
+ """Write the test body as SARIF without validating it first."""
+ (self.directory / "java.sarif").write_text(json.dumps(report), encoding="utf-8")
- def test_missing_report_or_wrong_language_fails(self):
- with self.assertRaises(ValueError):
- codeql.check(self.directory, "java-kotlin", 7)
- self.save(self.report)
- with self.assertRaises(ValueError):
- codeql.check(self.directory, "csharp", 7)
+ def test_clean_report_with_zero_findings_passes(self) -> None:
+ """Verify clean report with zero findings passes."""
+ self.save(self.report)
+ self.assertFalse(codeql.check(self.directory, "java-kotlin", 7))
- def test_empty_or_malformed_report_fails(self):
- for text in ["", "{", "null", "[]"]:
- with self.subTest(text=text):
- (self.directory / "java.sarif").write_text(text)
- with self.assertRaises((ValueError, AttributeError)):
- codeql.check(self.directory, "java-kotlin", 7)
+ def test_missing_report_or_wrong_language_fails(self) -> None:
+ """Verify missing report or wrong language fails."""
+ with self.assertRaises(ValueError):
+ codeql.check(self.directory, "java-kotlin", 7)
+ self.save(self.report)
+ with self.assertRaises(ValueError):
+ codeql.check(self.directory, "csharp", 7)
- def test_missing_runs_tool_rules_or_results_fails(self):
- variants = [{"version": "2.1.0"}, {"version": "2.1.0", "runs": []}]
- for field in ["tool", "results"]:
- bad = copy.deepcopy(self.report)
- del bad["runs"][0][field]
- variants.append(bad)
- bad = copy.deepcopy(self.report)
- bad["runs"][0]["tool"]["driver"]["rules"] = []
- variants.append(bad)
- for bad in variants:
- with self.subTest(report=bad):
- self.save(bad)
- with self.assertRaises(ValueError):
- codeql.check(self.directory, "java-kotlin", 7)
+ def test_empty_or_malformed_report_fails(self) -> None:
+ """Verify empty or malformed report fails."""
+ for text in ["", "{", "null", "[]"]:
+ with self.subTest(text=text):
+ (self.directory / "java.sarif").write_text(text)
+ with self.assertRaises((ValueError, AttributeError)):
+ codeql.check(self.directory, "java-kotlin", 7)
- def test_high_finding_blocks_and_unknown_rule_fails(self):
- self.report["runs"][0]["results"] = [{"ruleId": "synthetic"}]
- self.save(self.report)
- with contextlib.redirect_stdout(io.StringIO()):
- self.assertTrue(codeql.check(self.directory, "java-kotlin", 7))
- self.report["runs"][0]["results"] = [{"ruleId": "missing"}]
- self.save(self.report)
- with self.assertRaises(ValueError):
- codeql.check(self.directory, "java-kotlin", 7)
+ def test_missing_runs_tool_rules_or_results_fails(self) -> None:
+ """Verify missing runs tool rules or results fails."""
+ variants: list[dict[str, Any]] = [{"version": "2.1.0"}, {"version": "2.1.0", "runs": []}]
+ for field in ["tool", "results"]:
+ bad = copy.deepcopy(self.report)
+ del bad["runs"][0][field]
+ variants.append(bad)
+ bad = copy.deepcopy(self.report)
+ bad["runs"][0]["tool"]["driver"]["rules"] = []
+ variants.append(bad)
+ for bad in variants:
+ with self.subTest(report=bad):
+ self.save(bad)
+ with self.assertRaises(ValueError):
+ codeql.check(self.directory, "java-kotlin", 7)
- def test_failed_invocation_and_invalid_threshold_fail(self):
- self.report["runs"][0]["invocations"] = [{"executionSuccessful": False}]
- self.save(self.report)
- with self.assertRaises(ValueError):
- codeql.check(self.directory, "java-kotlin", 7)
- with self.assertRaises(ValueError):
- codeql.check(self.directory, "java-kotlin", float("nan"))
+ def test_high_finding_blocks_and_unknown_rule_fails(self) -> None:
+ """Verify high finding blocks and unknown rule fails."""
+ self.report["runs"][0]["results"] = [{"ruleId": "synthetic"}]
+ self.save(self.report)
+ with contextlib.redirect_stdout(io.StringIO()):
+ self.assertTrue(codeql.check(self.directory, "java-kotlin", 7))
+ self.report["runs"][0]["results"] = [{"ruleId": "missing"}]
+ self.save(self.report)
+ with self.assertRaises(ValueError):
+ codeql.check(self.directory, "java-kotlin", 7)
+
+ def test_failed_invocation_and_invalid_threshold_fail(self) -> None:
+ """Verify failed invocation and invalid threshold fail."""
+ self.report["runs"][0]["invocations"] = [{"executionSuccessful": False}]
+ self.save(self.report)
+ with self.assertRaises(ValueError):
+ codeql.check(self.directory, "java-kotlin", 7)
+ with self.assertRaises(ValueError):
+ codeql.check(self.directory, "java-kotlin", float("nan"))
if __name__ == "__main__":
- unittest.main()
+ unittest.main()
diff --git a/.github/scripts/test-script-quality.py b/.github/scripts/test-script-quality.py
new file mode 100644
index 0000000..48673bb
--- /dev/null
+++ b/.github/scripts/test-script-quality.py
@@ -0,0 +1,297 @@
+# Copyright 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+"""Prove the quality gates reject real violations, using private temporary fixtures and the locked tools."""
+
+import importlib.util
+import json
+import shutil
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from types import ModuleType
+from typing import Any, override
+from unittest import mock
+
+sys.dont_write_bytecode = True
+
+ROOT = Path(__file__).resolve().parents[2]
+GOOD_PYTHON = '"""Probe module."""\n\n\ndef add(left: int, right: int) -> int:\n """Add two integers."""\n return left + right\n'
+UNUSED_IMPORT_PYTHON = '"""Probe module."""\n\nimport os\n\n\ndef add(left: int, right: int) -> int:\n """Add two integers."""\n return left + right\n'
+TWO_SPACE_PYTHON = '"""Probe module."""\n\n\ndef add(left: int, right: int) -> int:\n """Add two integers."""\n return left + right\n'
+INCOMPATIBLE_PYTHON = '"""Probe module."""\n\nVALUE: int = "text"\n'
+UNANNOTATED_PYTHON = '"""Probe module."""\n\n\ndef echo(value):\n """Return the value."""\n return value\n'
+GOOD_JAVASCRIPT = "export function addNumbers(left, right)\n{\n return left + right;\n}\n"
+BAD_LAYOUT_JAVASCRIPT = "export function addNumbers(left, right) {\n return left + right\n}\n"
+GOOD_TYPED_JAVASCRIPT = "/** @type {number} */\nexport const value = 1;\n"
+INCOMPATIBLE_JAVASCRIPT = '/** @type {number} */\nexport const value = "text";\n'
+
+
+def load(name: str) -> ModuleType:
+ """Load the real sibling guard for tests."""
+ spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + ".py"))
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+guard = load("check-script-quality")
+
+
+def run(command: list[str], cwd: Path, stdin: str | None = None) -> subprocess.CompletedProcess[str]:
+ """Run one real tool invocation without a shell and capture its diagnostics."""
+ return subprocess.run(command, cwd=cwd, input=stdin, check=False, capture_output=True, text=True, encoding="utf-8") # noqa: S603 -- Fixed tool paths and private temporary arguments; no shell.
+
+
+def ruff(arguments: list[str], source: Path) -> subprocess.CompletedProcess[str]:
+ """Run the locked Ruff with the repository configuration on one private fixture."""
+ return run([sys.executable, "-m", "ruff", *arguments, "--no-cache", "--config", str(ROOT / "ruff.toml"), str(source)], source.parent)
+
+
+class ScopeTests(unittest.TestCase):
+ """Reject tracked sources that fall outside the explicit tool roots."""
+
+ def test_python_outside_script_root_is_rejected(self) -> None:
+ """Reject a tracked Python file outside the script root instead of letting it disappear."""
+ for stray in ("tools/helper.py", ".github/scripts/nested/helper.py", "helper.pyi"):
+ with self.subTest(stray=stray), self.assertRaisesRegex(ValueError, "outside the checked script root"):
+ guard.python_scope([".github/scripts/check-a.py", stray])
+
+ def test_python_scope_returns_sorted_script_root_and_rejects_empty(self) -> None:
+ """Select only the direct script root, sorted, and reject selecting nothing."""
+ self.assertEqual([".github/scripts/a.py", ".github/scripts/b.py"], guard.python_scope([".github/scripts/b.py", "README.md", ".github/scripts/a.py"]))
+ with self.assertRaisesRegex(ValueError, "No owned Python"):
+ guard.python_scope(["README.md"])
+
+ def test_javascript_requires_tsconfig_include_scope(self) -> None:
+ """Reject tracked JavaScript outside tsconfig include patterns, including .js/.cjs/.ts."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ (root / "tsconfig.json").write_text(json.dumps({"include": ["tools/**/*.mjs", "eslint.config.mjs"]}), encoding="utf-8")
+ self.assertEqual(["eslint.config.mjs", "tools/a/b.mjs", "tools/c.mjs"], guard.javascript_scope(root, ["tools/c.mjs", "tools/a/b.mjs", "eslint.config.mjs", "README.md"]))
+ for stray in ("src/z.js", "tools/w.cjs", "tools/v.ts", "tools/u.js"):
+ with self.subTest(stray=stray), self.assertRaisesRegex(ValueError, "outside the tsconfig include list"):
+ guard.javascript_scope(root, ["tools/c.mjs", stray])
+ with self.assertRaisesRegex(ValueError, "No owned JavaScript"):
+ guard.javascript_scope(root, ["README.md"])
+
+ def test_javascript_without_tsconfig_is_rejected(self) -> None:
+ """Reject JavaScript in a repository that has no tsconfig."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ self.assertEqual([], guard.javascript_scope(root, ["README.md"]))
+ with self.assertRaisesRegex(ValueError, "without a tsconfig.json"):
+ guard.javascript_scope(root, ["tools/a.mjs"])
+
+
+class PyrightCountTests(unittest.TestCase):
+ """Reject Pyright reports that analyzed too few files or found anything."""
+
+ @staticmethod
+ def report(analyzed: int, errors: int = 0, warnings: int = 0) -> dict[str, Any]:
+ """Build the exact Pyright JSON shape consumed by the guard."""
+ return {"summary": {"filesAnalyzed": analyzed, "errorCount": errors, "warningCount": warnings}, "generalDiagnostics": [{"file": "x.py", "message": "finding"}] * (errors + warnings), "exitCode": 1 if errors or warnings else 0}
+
+ def test_nonzero_exit_fails_even_with_clean_summary(self) -> None:
+ """Fail a nonzero Pyright exit even when every count is clean."""
+ clean = self.report(4)
+ clean["exitCode"] = 1
+ with self.assertRaisesRegex(ValueError, "exited 1"):
+ guard.check_pyright_report(clean, 4)
+
+ def test_count_must_match_selected_sources(self) -> None:
+ """Fail zero or mismatched analyzed counts even with no diagnostics."""
+ guard.check_pyright_report(self.report(4), 4)
+ for analyzed in (0, 3, 5):
+ with self.subTest(analyzed=analyzed), self.assertRaisesRegex(ValueError, "analyzed"):
+ guard.check_pyright_report(self.report(analyzed), 4)
+
+ def test_errors_and_warnings_fail(self) -> None:
+ """Fail on errors and on warnings at the exact count."""
+ for report in (self.report(4, errors=1), self.report(4, warnings=1)):
+ with self.subTest(report=report), self.assertRaisesRegex(ValueError, "reported findings"):
+ guard.check_pyright_report(report, 4)
+
+
+class RuffProbeTests(unittest.TestCase):
+ """Run the real locked Ruff on bounded private fixtures."""
+
+ def test_compliant_fixture_passes_and_violations_fail(self) -> None:
+ """Pass a compliant control and reject unused imports and wrong indentation."""
+ with tempfile.TemporaryDirectory() as name:
+ probe = Path(name) / "probe.py"
+ probe.write_text(GOOD_PYTHON, encoding="utf-8", newline="\n")
+ self.assertEqual(0, ruff(["check"], probe).returncode)
+ self.assertEqual(0, ruff(["format", "--check"], probe).returncode)
+ probe.write_text(UNUSED_IMPORT_PYTHON, encoding="utf-8", newline="\n")
+ unused = ruff(["check", "--output-format", "concise"], probe)
+ self.assertNotEqual(0, unused.returncode)
+ self.assertIn("F401", unused.stdout)
+ probe.write_text(TWO_SPACE_PYTHON, encoding="utf-8", newline="\n")
+ indented = ruff(["format", "--check"], probe)
+ self.assertNotEqual(0, indented.returncode)
+ self.assertIn("would be reformatted", (indented.stdout + indented.stderr).lower())
+
+
+class PyrightProbeTests(unittest.TestCase):
+ """Run the real locked Pyright with the repository strict settings on private projects."""
+
+ @staticmethod
+ def project(root: Path, include: list[str] | None = None) -> None:
+ """Write the repository Pyright config, without its venv location, into a private project."""
+ config: dict[str, Any] = json.loads((ROOT / ".github/pyrightconfig.json").read_text(encoding="utf-8"))
+ del config["venvPath"], config["venv"]
+ if include is not None:
+ config["include"] = include
+ (root / "pyrightconfig.json").write_text(json.dumps(config), encoding="utf-8")
+
+ def test_compliant_control_passes_and_strict_violations_fail(self) -> None:
+ """Analyze the control once with no findings and reject incompatible and unannotated code."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ self.project(root)
+ scripts = root / "scripts"
+ scripts.mkdir()
+ probe = scripts / "probe.py"
+ probe.write_text(GOOD_PYTHON, encoding="utf-8", newline="\n")
+ clean = guard.run_pyright(root, ".")
+ self.assertEqual(0, clean["exitCode"])
+ guard.check_pyright_report(clean, 1)
+ clean["exitCode"] = 1
+ with self.assertRaisesRegex(ValueError, "exited 1"):
+ guard.check_pyright_report(clean, 1)
+ probe.write_text(INCOMPATIBLE_PYTHON, encoding="utf-8", newline="\n")
+ report = guard.run_pyright(root, ".")
+ self.assertIn("reportAssignmentType", {item.get("rule") for item in report["generalDiagnostics"]})
+ with self.assertRaisesRegex(ValueError, "reported findings"):
+ guard.check_pyright_report(report, 1)
+ probe.write_text(UNANNOTATED_PYTHON, encoding="utf-8", newline="\n")
+ strict = guard.run_pyright(root, ".")
+ self.assertIn("reportMissingParameterType", {item.get("rule") for item in strict["generalDiagnostics"]})
+
+ def test_missing_locked_pyright_or_node_is_rejected(self) -> None:
+ """Reject a missing locked Pyright entry or Node instead of silently skipping the type gate."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ with mock.patch.object(guard, "PYRIGHT_ENTRY", root / "missing.js"), self.assertRaisesRegex(ValueError, "Locked Pyright is missing"):
+ guard.run_pyright(root, ".")
+ with mock.patch.object(guard.shutil, "which", return_value=None), self.assertRaisesRegex(ValueError, "Node.js is required"):
+ guard.run_pyright(root, ".")
+
+ def test_zero_analyzed_files_cannot_pass(self) -> None:
+ """Reject an empty include and a hidden-directory root config in the wrapper."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ scripts = root / ".github" / "scripts"
+ scripts.mkdir(parents=True)
+ (scripts / "probe.py").write_text(GOOD_PYTHON, encoding="utf-8", newline="\n")
+ for include in (["nothing/*.py"], [".github/scripts/*.py"]):
+ self.project(root, include)
+ with self.subTest(include=include), self.assertRaises(ValueError):
+ guard.check_pyright_report(guard.run_pyright(root, "."), 1)
+
+
+class JavaScriptProbeTests(unittest.TestCase):
+ """Run the real locked ESLint and TypeScript on bounded private input."""
+
+ @override
+ def setUp(self) -> None:
+ """Skip only where tsconfig is absent, because the scope guard forbids JavaScript there."""
+ if not (ROOT / "tsconfig.json").is_file():
+ self.skipTest("No JavaScript tooling in this repository")
+ node = shutil.which("node")
+ self.assertIsNotNone(node)
+ self.node = str(node)
+
+ def eslint(self, text: str) -> subprocess.CompletedProcess[str]:
+ """Lint text as an in-scope tool file using the repository configuration."""
+ return run([self.node, str(ROOT / "node_modules/eslint/bin/eslint.js"), "--stdin", "--stdin-filename", "tools/probe.mjs", "--max-warnings", "0", "--format", "json"], ROOT, text)
+
+ def test_eslint_rejects_wrong_layout(self) -> None:
+ """Pass a compliant control and reject K&R braces, two-space indent and a missing semicolon."""
+ good = self.eslint(GOOD_JAVASCRIPT)
+ self.assertEqual(0, good.returncode, good.stdout + good.stderr)
+ bad = self.eslint(BAD_LAYOUT_JAVASCRIPT)
+ self.assertNotEqual(0, bad.returncode)
+ rules = {message["ruleId"] for message in json.loads(bad.stdout)[0]["messages"]}
+ self.assertLessEqual({"@stylistic/brace-style", "@stylistic/semi", "@stylistic/indent"}, rules)
+
+ def test_checked_javascript_rejects_incompatible_assignment(self) -> None:
+ """Reject a real incompatible assignment under the repository strict checkJs settings."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ (root / "tsconfig.json").write_text(json.dumps({"extends": (ROOT / "tsconfig.json").as_posix(), "compilerOptions": {"typeRoots": [(ROOT / "node_modules/@types").as_posix()]}, "include": ["probe.mjs"]}), encoding="utf-8")
+ tsc = [self.node, str(ROOT / "node_modules/typescript/bin/tsc"), "-p", str(root / "tsconfig.json")]
+ (root / "probe.mjs").write_text(GOOD_TYPED_JAVASCRIPT, encoding="utf-8", newline="\n")
+ good = run(tsc, root)
+ self.assertEqual(0, good.returncode, good.stdout + good.stderr)
+ (root / "probe.mjs").write_text(INCOMPATIBLE_JAVASCRIPT, encoding="utf-8", newline="\n")
+ bad = run(tsc, root)
+ self.assertNotEqual(0, bad.returncode)
+ self.assertIn("TS2322", bad.stdout)
+
+
+URI_GATES = "jobs:\n pr:\n with:\n allow-dependencies-licenses: pkg:npm/uri-js\n"
+URI_MIXED_GATES = "jobs:\n pr:\n with:\n allow-dependencies-licenses: >-\n pkg:nuget/Microsoft.WindowsAppSDK,\n pkg:npm/uri-js\n"
+
+
+def uri_lock(version: str = "4.4.1", integrity: str | None = None, dev: bool = True, direct: bool = False, consumer_dev: bool = True) -> dict[str, Any]:
+ """Build a minimal package-lock shape with uri-js under an ESLint-like consumer."""
+ root_entry: dict[str, Any] = {"devDependencies": {"uri-js": version} if direct else {"eslint": "10.14.0"}}
+ return {"packages": {"": root_entry, "node_modules/uri-js": {"version": version, "integrity": integrity or guard.URI_JS_INTEGRITY, "dev": dev}, "node_modules/eslint": {"dev": consumer_dev, "dependencies": {"uri-js": "^4.2.2"}}}}
+
+
+def uri_project(root: Path, lock: dict[str, Any], gates: str = URI_GATES, license_bytes: bytes | None = None) -> None:
+ """Write a private project with the lock, installed LICENSE and PR caller that the guard inspects."""
+ (root / "package-lock.json").write_text(json.dumps(lock), encoding="utf-8")
+ installed = root / "node_modules" / "uri-js"
+ installed.mkdir(parents=True)
+ installed.joinpath("LICENSE").write_bytes((ROOT / "node_modules" / "uri-js" / "LICENSE").read_bytes() if license_bytes is None else license_bytes)
+ workflows = root / ".github" / "workflows"
+ workflows.mkdir(parents=True)
+ (workflows / "pr-gates.yml").write_text("# EX-0015 pkg:npm/uri-js is cited here\n" + gates, encoding="utf-8")
+
+
+class UriJsExceptionTests(unittest.TestCase):
+ """Allow the EX-0015 license exception only for the reviewed, unmodified, development-only uri-js."""
+
+ def test_reviewed_uri_js_with_exact_caller_is_accepted(self) -> None:
+ """Accept the reviewed package, installed license bytes and an exact caller list, mixed with NuGet entries."""
+ for gates in (URI_GATES, URI_MIXED_GATES):
+ with tempfile.TemporaryDirectory() as name, self.subTest(gates=gates):
+ root = Path(name)
+ uri_project(root, uri_lock(), gates)
+ guard.check_uri_js_exception(root)
+
+ def test_other_version_integrity_scope_or_consumer_is_rejected(self) -> None:
+ """Reject another version, integrity, non-dev or direct use, a non-dev consumer and a second copy."""
+ duplicate = uri_lock()
+ duplicate["packages"]["node_modules/eslint/node_modules/uri-js"] = dict(duplicate["packages"]["node_modules/uri-js"])
+ for lock in (uri_lock(version="4.4.2"), uri_lock(integrity="sha512-other"), uri_lock(dev=False), uri_lock(direct=True), uri_lock(consumer_dev=False), duplicate):
+ with tempfile.TemporaryDirectory() as name, self.subTest(lock=lock), self.assertRaisesRegex(ValueError, "uri-js"):
+ root = Path(name)
+ uri_project(root, lock)
+ guard.check_uri_js_exception(root)
+
+ def test_modified_installed_license_is_rejected(self) -> None:
+ """Reject installed license bytes that differ from the reviewed copy."""
+ with tempfile.TemporaryDirectory() as name:
+ root = Path(name)
+ uri_project(root, uri_lock(), license_bytes=(ROOT / "node_modules" / "uri-js" / "LICENSE").read_bytes() + b"\n")
+ with self.assertRaisesRegex(ValueError, "LICENSE bytes differ"):
+ guard.check_uri_js_exception(root)
+
+ def test_caller_must_list_only_uri_js(self) -> None:
+ """Reject a caller that lists no npm exception, another package, a versioned form or a duplicate."""
+ for gates in ("jobs:\n pr:\n uses: shared\n", URI_GATES.rstrip() + ", pkg:npm/other\n", "jobs:\n pr:\n with:\n allow-dependencies-licenses: pkg:npm/other\n", URI_GATES.rstrip() + "@4.4.1\n", URI_GATES.rstrip() + ", pkg:npm/uri-js\n"):
+ with tempfile.TemporaryDirectory() as name, self.subTest(gates=gates), self.assertRaisesRegex(ValueError, "permits only"):
+ root = Path(name)
+ uri_project(root, uri_lock(), gates)
+ guard.check_uri_js_exception(root)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/scripts/test-source-headers.py b/.github/scripts/test-source-headers.py
index 8a04b44..20102c8 100644
--- a/.github/scripts/test-source-headers.py
+++ b/.github/scripts/test-source-headers.py
@@ -11,7 +11,6 @@ import unittest
from pathlib import Path
from typing import Any
-
SPEC = importlib.util.spec_from_file_location("headers", Path(__file__).with_name("check-source-headers.py"))
assert SPEC is not None and SPEC.loader is not None
HEADERS = importlib.util.module_from_spec(SPEC)
@@ -32,8 +31,7 @@ limitations under the License."""
def fixture_policy(holder: str = "James Maes", license_id: str = "MIT") -> dict[str, Any]:
"""Use the production styles with a small explicit owned fixture scope."""
- return {"includes": ["src/*"], "exclusions": [], "styles": POLICY["styles"],
- "notices": [{"holder": holder, "license": license_id, "paths": ["src/*"]}]}
+ return {"includes": ["src/*"], "exclusions": [], "styles": POLICY["styles"], "notices": [{"holder": holder, "license": license_id, "paths": ["src/*"]}]}
class HeaderTests(unittest.TestCase):
@@ -51,12 +49,23 @@ class HeaderTests(unittest.TestCase):
with self.subTest(name=name):
HEADERS.check_notice(name, text, fixture_policy())
+ def test_year_range_separator_is_hyphen_or_en_dash_only(self) -> None:
+ """Accept a hyphen or en dash between years and reject any other separator."""
+ for separator, accepted in (("-", True), (chr(0x2013), True), ("/", False), (chr(0x2014), False)):
+ text = f"# Copyright 2025{separator}2026 James Maes\n# SPDX-License-Identifier: MIT"
+ with self.subTest(separator=separator):
+ if accepted:
+ HEADERS.check_notice("src/a.py", text, fixture_policy())
+ else:
+ with self.assertRaises(ValueError):
+ HEADERS.check_notice("src/a.py", text, fixture_policy())
+
def test_required_first_lines_and_bom(self) -> None:
"""Accept shebangs, PowerShell requirements and XML declarations first."""
for name, prefix, body in (
("src/a.sh", "#!/bin/sh\n", "# " + MIT.replace("\n", "\n# ")),
("src/a.ps1", "#Requires -Version 7.4\n", "# " + MIT.replace("\n", "\n# ")),
- ("src/a.xml", "\n", ""),
+ ("src/a.xml", '\n', ""),
("src/a.mjs", "#!/usr/bin/env node\n", "/*" + MIT + "*/"),
):
with self.subTest(name=name):
@@ -67,21 +76,20 @@ class HeaderTests(unittest.TestCase):
for name, text in (
("src/a.sh", "# " + MIT.replace("\n", "\n# ") + "\n#!/bin/sh"),
("src/a.ps1", "# " + MIT.replace("\n", "\n# ") + "\n#Requires -Version 7.4"),
- ("src/a.xml", ""),
+ ("src/a.xml", "'),
):
with self.subTest(name=name), self.assertRaises(ValueError):
HEADERS.check_notice(name, text, fixture_policy())
def test_code_strings_and_late_comments_do_not_count(self) -> None:
"""Reject docstrings and comments after executable declarations."""
- for text in ("notice = \"" + MIT.replace("\n", "\\n") + "\"",
- "\"\"\"" + MIT + "\"\"\"", "import os\n# " + MIT.replace("\n", "\n# ")):
+ for text in ('notice = "' + MIT.replace("\n", "\\n") + '"', '"""' + MIT + '"""', "import os\n# " + MIT.replace("\n", "\n# ")):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.py", text, fixture_policy())
def test_wrong_syntax_and_unterminated_comments_fail(self) -> None:
"""Notice text alone is insufficient without a complete language comment."""
- for text in ("# " + MIT, "/*" + MIT, "string text = \"" + MIT + "\";"):
+ for text in ("# " + MIT, "/*" + MIT, 'string text = "' + MIT + '";'):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.cs", text, fixture_policy())
with self.assertRaises(ValueError):
@@ -89,8 +97,7 @@ class HeaderTests(unittest.TestCase):
def test_wrong_holder_or_license_and_conflicting_spdx_fail(self) -> None:
"""Do not conflate repository licenses or accept conflicting identifiers."""
- for text in (MIT.replace("James Maes", "Someone Else"), MIT.replace("MIT", "Apache-2.0"),
- MIT + "\nSPDX-License-Identifier: Apache-2.0", MIT.splitlines()[0]):
+ for text in (MIT.replace("James Maes", "Someone Else"), MIT.replace("MIT", "Apache-2.0"), MIT + "\nSPDX-License-Identifier: Apache-2.0", MIT.splitlines()[0]):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.cs", "/*" + text + "*/", fixture_policy())
@@ -103,8 +110,7 @@ class HeaderTests(unittest.TestCase):
def test_java_requires_full_apache_block(self) -> None:
"""An abbreviated SPDX-only Java notice cannot replace scoped convention."""
policy = fixture_policy("Kingsrook, LLC", "Apache-2.0")
- for notice in ("Copyright 2026 Kingsrook, LLC\nSPDX-License-Identifier: Apache-2.0",
- APACHE.replace("limitations under the License.", "")):
+ for notice in ("Copyright 2026 Kingsrook, LLC\nSPDX-License-Identifier: Apache-2.0", APACHE.replace("limitations under the License.", "")):
with self.subTest(notice=notice), self.assertRaises(ValueError):
HEADERS.check_notice("src/A.java", "/*" + notice + "*/", policy)
@@ -121,9 +127,7 @@ class HeaderTests(unittest.TestCase):
"""New scripts enter the guard; data, generated and upstream files do not."""
self.assertTrue(HEADERS.selected(".github/scripts/new-tool.py", POLICY))
self.assertTrue(HEADERS.selected(".github/workflows/new-check.yml", POLICY))
- for name in (".mvn/wrapper/MavenWrapperDownloader.java", "mvnw", "schemas/new.json",
- "src/main/resources/openapi/new.yaml", "src/test/resources/openapi/old.yaml",
- "src/generated/Example.cs", "src/Example/obj/Generated.cs", "packages.lock.json"):
+ for name in (".mvn/wrapper/MavenWrapperDownloader.java", "mvnw", "schemas/new.json", "src/main/resources/openapi/new.yaml", "src/test/resources/openapi/old.yaml", "src/generated/Example.cs", "src/Example/obj/Generated.cs", "packages.lock.json"):
with self.subTest(name=name):
self.assertFalse(HEADERS.selected(name, POLICY))
self.assertTrue(all(item["reason"].strip() for item in POLICY["exclusions"]))
@@ -140,11 +144,11 @@ class HeaderTests(unittest.TestCase):
"""A tracked new source fails while untracked generated output is ignored."""
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
- subprocess.run(["git", "init", "-q", str(root)], check=True)
+ subprocess.run(["git", "init", "-q", str(root)], check=True) # noqa: S603, S607 -- Fixed Git command in the owned temporary test directory.
root.joinpath("src").mkdir()
source = root / "src/a.py"
source.write_text("print(1)", encoding="utf-8")
- subprocess.run(["git", "-C", str(root), "add", "--", "src/a.py"], check=True)
+ subprocess.run(["git", "-C", str(root), "add", "--", "src/a.py"], check=True) # noqa: S603, S607 -- Fixed synthetic path; never a shell command.
with self.assertRaisesRegex(ValueError, "src/a.py"):
HEADERS.check_repository(root, fixture_policy())
source.write_text("# " + MIT.replace("\n", "\n# ") + "\nprint(1)", encoding="utf-8")
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3d3bb4d..3c3e578 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,6 +26,19 @@ jobs:
node-version: '24'
package-manager-cache: false
- run: npm ci --ignore-scripts
+ - name: Set up uv
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
+ with:
+ version: '0.13.6'
+ enable-cache: false
+ - name: Restore locked Python tools
+ run: uv sync --locked
+ - name: Verify script quality gates and rejection paths
+ run: |
+ uv run --locked --no-sync python .github/scripts/test-script-quality.py
+ uv run --locked --no-sync python .github/scripts/check-script-quality.py
+ npm run typecheck
+ npm run lint
- name: Verify owned source notices and rejection paths
run: |
python3 .github/scripts/test-source-headers.py
diff --git a/.github/workflows/pr-gates.yml b/.github/workflows/pr-gates.yml
index b43d1fa..cc10d2e 100644
--- a/.github/workflows/pr-gates.yml
+++ b/.github/workflows/pr-gates.yml
@@ -18,6 +18,12 @@ concurrency:
jobs:
pr:
uses: KofTwentyTwo/standards/.github/workflows/pr.yml@fdf5ed029603c938615a32b1beca3628b8787945 # v0.1.2
+ with:
+ # EX-0015: development-only uri-js 4.4.1 (ESLint transitive; BSD-2-Clause plus an authors' views sentence).
+ # https://git.kof22.com/KofTwentyTwo/standards/src/commit/a18179e90f8e6d35ba9f5e583249885c066e52b8/exceptions/register.md#ex-0015
+ # The matcher ignores versions; check-script-quality.py pins the version, lock integrity and installed
+ # license bytes and rejects any other pkg:npm entry here.
+ allow-dependencies-licenses: pkg:npm/uri-js
permissions:
contents: read # read the commits being merged
pull-requests: read # read the pull request title and dependency diff
diff --git a/.gitignore b/.gitignore
index b0099ef..0a81630 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,8 @@ dist/
.env
.DS_Store
Thumbs.db
+
+# Local script-quality environments and tool caches.
+.venv/
+.ruff_cache/
+__pycache__/
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..24ee5b1
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1946377..cd7c315 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,9 @@
acceptance remain required before a coordinated release.
- Add the [extension version registry](docs/VERSIONS.md). No schema or fixture shape
changed for the 2.2 to 2.3 reservation.
+- Add `LABEL_4X2_*` and `LABEL_4X1_*` test profiles (203/300/600 dpi) with a 4 × 1
+ fixture; the 4 × 2 and 4 × 1 PDFs carry explicit actual-size job paper and never a
+ `mediaProfile`. Both consumers must come from this cohort.
## 0.3.0-rc.1 — 2026-10-31
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b66c37d..c76e3c4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-Use Node.js 24 or newer. Install the locked development dependencies with `npm ci`, then run `npm run check`. The runtime protocol has no JavaScript dependency; Ajv and ajv-formats are development-only JSON Schema validators.
+Use Node.js 24 or newer. Install the locked development dependencies with `npm ci`, then run `npm run check` (`npm test` is the standard entry point for the same check). The runtime protocol has no JavaScript dependency; Ajv and ajv-formats are development-only JSON Schema validators.
Make changes through a pull request. Describe the client/server behavior affected and update README, schema, fixtures and changelog together. Keep credentials, customer documents, printer addresses and machine-specific data out of fixtures. Samples must remain synthetic.
@@ -9,3 +9,18 @@ Run both the Java and .NET consumers' protocol tests for a contract change. Chec
Keep wire names stable within `/printing/v1`. Optional response fields may be added; clients ignore unknown response fields. Removing/renaming a field, changing its type or changing an existing field's meaning requires a new protocol version. A prerelease package version is not permission to silently break an already deployed connector.
`npm run bundle` produces the release archive and checksum in `dist/`. Do not hand-edit generated checksums. Release tags must match the version in package.json, prefixed with `v`. Commits and releases are made by maintainers after review.
+
+## Script quality
+
+Owned scripts follow the KofTwentyTwo standard at revision `fbfc8d2cf3b5e16ae9750b04cf9de1794f1e7152` (configs adapted from its `python` and `typescript` profiles): three-space indentation, Ruff 0.17.3 format/lint (locked in `uv.lock`) and strict Pyright 1.1.414 (Microsoft's npm package, locked in `package-lock.json` and run through Node) for `.github/scripts/*.py`, and ESLint 10 with Stylistic (Allman) plus strict `tsc` over JSDoc for `tools/**/*.mjs` and `eslint.config.mjs`. Restore with `uv sync --locked` and `npm ci --ignore-scripts`, then run:
+
+```text
+uv run --locked --no-sync python .github/scripts/test-script-quality.py
+uv run --locked --no-sync python .github/scripts/check-script-quality.py
+npm run typecheck
+npm run lint
+```
+
+`npm run quality` runs the repository's Python sequence in one step (`uv sync --locked`, both script-quality commands and both source-header commands); the shared Node CI calls it as its `quality-script`. It needs `uv` on `PATH` and `npm ci` first.
+
+`check-script-quality.py` fails for any tracked Python outside `.github/scripts/` or JavaScript outside the `tsconfig.json` include list, and requires Pyright's analyzed-file count to equal the selected scripts. Pyright must run as `pyright -p .github`: it silently skips dot directories from a root config. Documented exceptions: CLI `print`/`console.log` only in `check-*.py` and `tools/`; synthetic test assertions and boundary constants; narrow inline `noqa` comments for fixed no-shell Git/interpreter calls and ordered fail-closed validators. Do not loosen a rule or add a type waiver to pass. `package.json` overrides `minimatch` to 11.0.1, the patched release for [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (affected before 11.0.1); keep it until ESLint's own dependency range includes a patched release. The only npm license exception, [EX-0015](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0015) for development-only `uri-js` 4.4.1, is enforced by `check-script-quality.py` (version, lock integrity, installed license bytes and the exact `pkg:npm/uri-js` caller entry); changing that package or the entry needs renewed approval.
diff --git a/README.md b/README.md
index a7f80de..f811313 100644
--- a/README.md
+++ b/README.md
@@ -77,7 +77,7 @@ polling, and explicit receipts. These additions are included in the
* `EnrollResponse`: `protocolVersion` (1), `deviceId`, `deviceToken`, `pollIntervalSeconds` (default 5).
* `HeartbeatRequest`: `agentVersion`. Response: `serverTime`.
* `PrinterInventory`: full snapshot of the queues selected for cloud exposure in the local Windows companion, in `printers`. Each printer contains stable `printerId`, exact Windows queue `name`, `driverName`, `portName`, `status`, `formats`. An empty list withdraws every queue. There is no new required exposure field: presence in the snapshot expresses local exposure.
-* `PrintOptions`: `copies` defaults to 1; `scale` defaults to `ACTUAL_SIZE`. The schema reserves optional `paperWidthMm`, `paperHeightMm` (which must appear together), but the initial implementation rejects them until per-job paper configuration is implemented. Configure paper on the Windows queue for now. First implementation supports `ACTUAL_SIZE` and `FIT`. ZPL dimensions are encoded in its payload; copies must be applied exactly once.
+* `PrintOptions`: `copies` defaults to 1; `scale` defaults to `ACTUAL_SIZE`. Optional `paperWidthMm`, `paperHeightMm` (which must appear together) request actual-size job paper for PDF jobs; ZPL encodes its dimensions in the payload, and a `mediaProfile` selection cannot be combined with them. Without them, the Windows queue default applies. First implementation supports `ACTUAL_SIZE` and `FIT`. ZPL dimensions are encoded in its payload; copies must be applied exactly once.
* `SubmitJobRequest`: `deviceId`, `printerId`, `title`, `format`, `contentBase64`, `idempotencyKey`, optional `options`.
* `PrintJob`: `jobId`, `printerId`, `title`, `format`, lowercase hex `contentSha256`, `contentLength` (decoded bytes), normalized `options`, `createdAt`, `status`. Management responses additionally include `deviceId` and may include latest `message` and `spoolerJobId`.
* `JobEvent`: stable `eventId`, `status`, optional `message`, optional string `spoolerJobId`.
@@ -161,7 +161,7 @@ Error body: `{ "code": "ERROR_CODE", "message": "Human-readable explanation" }`.
## Branded test-document profiles
-`DeviceTestPrintRequest` optionally includes `testProfile`. PDF accepts `LETTER`, `A4`, or a `LABEL_4X6_*` profile (all label profiles produce the same 101.6 × 152.4 mm PDF). ZPL accepts `LABEL_4X6_203`, `LABEL_4X6_300`, or `LABEL_4X6_600`; the suffix selects the printer resolution in dots per inch. Use possible-value selectors in operator interfaces and match the loaded stock and device resolution. The profile defines document layout; it does not reconfigure Windows paper stock.
+`DeviceTestPrintRequest` optionally includes `testProfile`. PDF accepts `LETTER`, `A4`, or a `LABEL_4X6_*`, `LABEL_4X2_*` or `LABEL_4X1_*` profile (all profiles of one height produce the same PDF: 101.6 × 152.4, 50.8 or 25.4 mm). ZPL accepts those label profiles with `_203`, `_300` or `_600`; the suffix selects the printer resolution in dots per inch. For the 4 × 2 and 4 × 1 PDFs the server also submits `options.paperWidthMm`/`paperHeightMm` (101.6 × 50.8 or 25.4) so the client sets actual-size job paper; they are never combined with `mediaProfile`. The 4 × 6 PDF and all ZPL labels carry no job paper. These profiles require an agent and server from the same unreleased 0.4 cohort; older agents reject job paper. Use possible-value selectors in operator interfaces and match the loaded stock and device resolution. The profile defines document layout; it does not reconfigure Windows paper stock.
Requests that omit the field retain compatible defaults: PDF uses Letter, ZPL uses 4 × 6 at 203 dpi. An explicit null, unknown profile, or Letter/A4 with ZPL is invalid. A stable requestId protects transport retries; changing document media is a new operator request. Each action queues one test on each explicitly selected destination, subject to the existing exposure and approval checks. No discovery or connection event automatically prints.
diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md
index 02c894f..0db4e22 100644
--- a/docs/SESSION-STATE.md
+++ b/docs/SESSION-STATE.md
@@ -1,6 +1,25 @@
# Session state — 2026-11-01
-## Echo printer media contract in progress
+## Release-cohort integration (branch `release/printing-cohort-20261101`)
+
+Unreleased cohort: protocol package `0.4.0-rc.1`, wire v1, native printing extension
+2.3.0. This branch integrates the script-quality tooling (locked Ruff/Pyright,
+ESLint/Stylistic, strict `checkJs`, the `uri-js` license guard) with the short-label
+test profiles. Short labels add `LABEL_4X2_*` and `LABEL_4X1_*` (203/300/600 dpi)
+for ZPL and PDF, a `device-test-profile-4x1.request.json` fixture, and explicit
+actual-size job paper (101.6 × 50.8 or 25.4 mm) for the 4 × 2 and 4 × 1 PDFs, which
+is never combined with `mediaProfile`. The strictly typed tools keep every existing
+invalid-input check and add the short-label positive, unsupported-height and
+job-paper assertions. Expected totals are 76 schema definitions and 79 fixtures.
+
+State: implemented and locally verified on the merged tree. `npm run check` passed
+76 schema definitions and 79 fixtures, and `npm run typecheck`, `npm run lint` and
+`npm run bundle` all exited 0. Hosted Gitea CI, consumer pin updates, installed-client
+acceptance and physical output are pending; nothing here claims them. The consumer
+repositories pin this protocol by immutable commit after the final protocol commit
+exists.
+
+## Echo printer media contract in progress (earlier checkpoint)
Isolated branch `feat/echo-media-contract-20261101` adds configured Windows
queue-default profiles and an exact ID/revision job selection. See
diff --git a/docs/TODO.md b/docs/TODO.md
index b8fe97e..b87a1fa 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1,5 +1,17 @@
# TODO
+## Script quality gates
+
+- [x] Pin Ruff, strict Pyright, ESLint/Stylistic and strict checkJs with locked restores; reject untracked-scope Python/JavaScript and zero-file Pyright runs; negative tool probes pass locally.
+- [ ] Observe the new `verify` job steps pass on hosted CI before relying on them.
+
+## Release cohort integration (0.4.0-rc.1, unreleased)
+
+- [x] Integrate short-label (4 × 2 / 4 × 1) test profiles, fixture and job-paper assertions with the strictly typed tools in source.
+- [x] Pass the merged-tree `npm run check` (76 definitions / 79 fixtures), `npm run typecheck`, `npm run lint` and `npm run bundle`.
+- [ ] Pin the final protocol commit in both consumers, then observe hosted Gitea CI.
+- [ ] Complete installed client and physical short-label acceptance separately; schema checks do not establish them.
+
## Echo media integration
- [x] Define bounded queue-default profile and exact revision selection schemas.
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..3d2609d
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,84 @@
+// Copyright 2026 Kingsrook, LLC
+// SPDX-License-Identifier: Apache-2.0
+// Based on K22 fbfc8d2cf3b5e16ae9750b04cf9de1794f1e7152.
+// KofTwentyTwo TypeScript / JavaScript lint configuration (K22-CODE-TS).
+// The Kingsrook layout (3-space indent, Allman braces, if(...) with no space, no more than
+// one blank line inside code) is enforced by ESLint Stylistic rather than Prettier, which
+// cannot express Allman braces. Type-aware strict rules come from typescript-eslint.
+// Install: npm i -D eslint @eslint/js typescript-eslint @stylistic/eslint-plugin typescript
+import js from "@eslint/js";
+import stylistic from "@stylistic/eslint-plugin";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ {
+ ignores: ["**", "!tools/", "!tools/**", "!eslint.config.mjs"],
+ },
+ js.configs.recommended,
+ tseslint.configs.strictTypeChecked,
+ tseslint.configs.stylisticTypeChecked,
+ {
+ languageOptions: {
+ globals: { process: "readonly", console: "readonly", Buffer: "readonly", URL: "readonly", fetch: "readonly", WebSocket: "readonly", setTimeout: "readonly", clearTimeout: "readonly", structuredClone: "readonly", performance: "readonly", AbortController: "readonly", Headers: "readonly" },
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ plugins: { "@stylistic": stylistic },
+ rules: {
+ // Kingsrook layout
+ "@stylistic/indent": ["error", 3, { SwitchCase: 1 }],
+ "@stylistic/brace-style": ["error", "allman", { allowSingleLine: false }],
+ "@stylistic/keyword-spacing": ["error", {
+ before: true,
+ after: true,
+ overrides: {
+ if: { after: false },
+ for: { after: false },
+ while: { after: false },
+ switch: { after: false },
+ catch: { after: false },
+ },
+ }],
+ // 1 blank line inside code; up to 3 so methods can be separated by 3.
+ "@stylistic/no-multiple-empty-lines": ["error", { max: 3, maxBOF: 0, maxEOF: 0 }],
+ "@stylistic/lines-between-class-members": ["error", "always", { exceptAfterSingleLine: true }],
+ "@stylistic/semi": ["error", "always"],
+ "@stylistic/quotes": ["error", "double", { avoidEscape: true }],
+ "@stylistic/comma-dangle": ["error", "always-multiline"],
+ "@stylistic/eol-last": ["error", "always"],
+ "@stylistic/no-tabs": "error",
+ "@stylistic/no-trailing-spaces": "error",
+ "@stylistic/operator-linebreak": ["error", "before"],
+ "@stylistic/dot-location": ["error", "property"],
+ "@stylistic/max-len": "off",
+
+ // Kingsrook non-layout rules
+ "curly": ["error", "all"],
+ "eqeqeq": ["error", "always"],
+ "no-console": "error",
+ "no-warning-comments": ["error", { terms: ["todo", "fixme"], location: "start" }],
+ "@typescript-eslint/explicit-function-return-type": "error",
+ "@typescript-eslint/naming-convention": ["error",
+ { selector: "default", format: ["camelCase"] },
+ { selector: "variable", modifiers: ["const", "global"], format: ["camelCase", "UPPER_CASE"] },
+ { selector: "typeLike", format: ["PascalCase"] },
+ { selector: "enumMember", format: ["PascalCase", "UPPER_CASE"] },
+ { selector: "import", format: null },
+ { selector: "objectLiteralProperty", format: null },
+ ],
+ },
+ },
+ {
+ files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
+ extends: [tseslint.configs.disableTypeChecked],
+ // Checked JavaScript uses JSDoc plus strict tsc; TS-only return syntax is unavailable.
+ rules: { "@typescript-eslint/explicit-function-return-type": "off" },
+ },
+ {
+ files: ["tools/**/*.mjs"],
+ // These command-line tools intentionally print their results and diagnostics.
+ rules: { "no-console": ["error", { allow: ["log", "error"] }] },
+ },
+);
diff --git a/fixtures/device-test-profile-4x1.request.json b/fixtures/device-test-profile-4x1.request.json
new file mode 100644
index 0000000..5a4b8df
--- /dev/null
+++ b/fixtures/device-test-profile-4x1.request.json
@@ -0,0 +1 @@
+{"printerId":"printer-zebra","format":"ZPL","requestId":"operator-branded-test-4x1","testProfile":"LABEL_4X1_300"}
diff --git a/package-lock.json b/package-lock.json
index a773023..221952b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,13 +9,587 @@
"version": "0.4.0-rc.1",
"license": "Apache-2.0",
"devDependencies": {
+ "@eslint/js": "10.0.1",
+ "@stylistic/eslint-plugin": "5.10.0",
+ "@types/node": "24.10.1",
"ajv": "8.20.0",
- "ajv-formats": "3.0.1"
+ "ajv-formats": "3.0.1",
+ "eslint": "10.14.0",
+ "pyright": "1.1.414",
+ "typescript": "6.0.3",
+ "typescript-eslint": "8.72.1"
},
"engines": {
"node": ">=24"
}
},
+ "node_modules/@cacheable/memory": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
+ "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/utils": "^2.5.0",
+ "@keyv/bigmap": "^1.3.1",
+ "hookified": "^1.15.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
+ "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.5.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
+ "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
+ "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/nonrecursive-glob-match": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz",
+ "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/@keyv/bigmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+ "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.4.0",
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@stylistic/eslint-plugin": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
+ "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/types": "^8.56.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "estraverse": "^5.3.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz",
+ "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz",
+ "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.72.1",
+ "@typescript-eslint/type-utils": "8.72.1",
+ "@typescript-eslint/utils": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.72.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
+ "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.72.1",
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
+ "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.72.1",
+ "@typescript-eslint/types": "^8.72.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz",
+ "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz",
+ "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
+ "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1",
+ "@typescript-eslint/utils": "8.72.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
+ "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
+ "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.72.1",
+ "@typescript-eslint/tsconfig-utils": "8.72.1",
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/visitor-keys": "8.72.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz",
+ "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.72.1",
+ "@typescript-eslint/types": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
+ "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
+ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -51,6 +625,306 @@
}
}
},
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/cacheable": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
+ "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/memory": "^2.2.0",
+ "@cacheable/utils": "^2.5.0",
+ "hookified": "^1.15.0",
+ "keyv": "^5.6.0",
+ "qified": "^0.10.1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
+ "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.7.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.3",
+ "@humanfs/node": "^0.16.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "11.1.5 || >11.1.6 <12",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.6",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -58,6 +932,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-uri": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
@@ -75,6 +963,168 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "11.1.5",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
+ "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.23"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "6.1.23",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
+ "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^2.5.0",
+ "flatted": "^3.4.2",
+ "hookified": "^1.15.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hashery": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
+ "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hookified": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
+ "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -82,6 +1132,224 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.1.tgz",
+ "integrity": "sha512-t+Vn6DX4wPCxS23y03C0uhF7S1jpi1GfqPrnlEipCJaA2ykiNbMkVlDutZtmvLfRcUN6R8AYVlzTsQrT4RZoLQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/nonrecursive-glob-match": "^1.0.1",
+ "brace-expansion": "^5.0.12"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pyright": {
+ "version": "1.1.414",
+ "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.414.tgz",
+ "integrity": "sha512-FPZZb51jepDX4eP7TEYDeNFtmE3WgwkkEcJpvH3/QmUSsj0EAy3LXu+xB4T/FWDejtsXlCfFr/rbWFjwuwuXww==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pyright": "index.js",
+ "pyright-langserver": "langserver.index.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/qified": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
+ "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/qified/node_modules/hookified": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
+ "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -91,6 +1359,179 @@
"engines": {
"node": ">=0.10.0"
}
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.72.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz",
+ "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.72.1",
+ "@typescript-eslint/parser": "8.72.1",
+ "@typescript-eslint/typescript-estree": "8.72.1",
+ "@typescript-eslint/utils": "8.72.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/package.json b/package.json
index d5cc3a2..90a2e80 100644
--- a/package.json
+++ b/package.json
@@ -10,10 +10,25 @@
},
"scripts": {
"check": "node tools/verify-schema.mjs && node tools/verify-fixtures.mjs",
- "bundle": "node tools/bundle.mjs"
+ "test": "npm run check",
+ "bundle": "node tools/bundle.mjs",
+ "lint": "eslint . --max-warnings 0",
+ "typecheck": "tsc --noEmit",
+ "quality": "uv sync --locked && uv run --locked --no-sync python .github/scripts/test-script-quality.py && uv run --locked --no-sync python .github/scripts/check-script-quality.py && uv run --locked --no-sync python .github/scripts/test-source-headers.py && uv run --locked --no-sync python .github/scripts/check-source-headers.py"
},
"devDependencies": {
"ajv": "8.20.0",
- "ajv-formats": "3.0.1"
- }
+ "ajv-formats": "3.0.1",
+ "@eslint/js": "10.0.1",
+ "typescript": "6.0.3",
+ "@stylistic/eslint-plugin": "5.10.0",
+ "@types/node": "24.10.1",
+ "typescript-eslint": "8.72.1",
+ "eslint": "10.14.0",
+ "pyright": "1.1.414"
+ },
+ "overrides": {
+ "minimatch": "11.0.1"
+ },
+ "packageManager": "npm@11.19.1"
}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..60be067
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,13 @@
+# Copyright 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+[project]
+name = "qqq-print-protocol-script-quality"
+version = "0.0.0"
+requires-python = ">=3.13"
+
+[dependency-groups]
+dev = ["ruff==0.17.3"]
+
+[tool.uv]
+package = false
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..286fa55
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,56 @@
+# Copyright 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+# Based on K22 fbfc8d2cf3b5e16ae9750b04cf9de1794f1e7152.
+# KofTwentyTwo Python lint + format configuration (K22-CODE-PY). Copy to the repo root
+# (or merge into [tool.ruff] in pyproject.toml). `ruff format --check` and `ruff check`
+# run in CI and pre-commit with zero findings allowed.
+target-version = "py313"
+include = [".github/scripts/*.py"]
+extend-exclude = [".venv", "node_modules", "dist", "target", "bin", "obj"]
+# Kingsrook: no practical line-length limit (ruff's maximum is 320).
+line-length = 320
+# Kingsrook: 3-space indentation. Ruff's formatter supports it fully; pycodestyle's
+# "multiple of four" rules (E111/E114) are therefore not selected.
+indent-width = 3
+
+[format]
+quote-style = "double"
+indent-style = "space"
+line-ending = "lf"
+docstring-code-format = true
+
+[lint]
+select = [
+ "F", # pyflakes
+ "E4", "E7", "E9", "W", # pycodestyle errors that are layout-independent
+ "I", # isort: no wildcard imports, ordered
+ "N", # pep8-naming
+ "UP", # pyupgrade
+ "B", # bugbear
+ "S", # bandit security
+ "BLE", # blind except
+ "T20", # print statements: use logging (Kingsrook logging rule)
+ "ERA", # commented-out code (Kingsrook: no zombie code)
+ "FIX", # FIXME/TODO must be resolved or tracked
+ "TD", # TODO format: author and issue link
+ "PTH", # pathlib over os.path
+ "RET", "SIM", "C4", "PIE", "PL", "RUF",
+ "ANN", # type annotations on every function
+ "D", # docstrings on modules, classes, functions
+ "LOG", "G", # logging hygiene
+]
+ignore = [
+ "D203", # conflicts with D211
+ "D213", # conflicts with D212
+ "TD003", # issue link is enforced by review, not regex
+]
+
+[lint.per-file-ignores]
+".github/scripts/check-*.py" = ["T201"] # CLI diagnostics are the public stdout contract, not application logging.
+".github/scripts/test-*.py" = ["S101", "PLR2004"] # Synthetic assertions and explicit boundary values.
+
+[lint.pydocstyle]
+convention = "pep257"
+
+[lint.flake8-tidy-imports]
+ban-relative-imports = "all"
diff --git a/schemas/protocol.schema.json b/schemas/protocol.schema.json
index cbfec3c..ae7be6a 100644
--- a/schemas/protocol.schema.json
+++ b/schemas/protocol.schema.json
@@ -65,7 +65,13 @@
"enum": [
"LABEL_4X6_203",
"LABEL_4X6_300",
- "LABEL_4X6_600"
+ "LABEL_4X6_600",
+ "LABEL_4X2_203",
+ "LABEL_4X2_300",
+ "LABEL_4X2_600",
+ "LABEL_4X1_203",
+ "LABEL_4X1_300",
+ "LABEL_4X1_600"
]
}
}
@@ -79,7 +85,13 @@
"A4",
"LABEL_4X6_203",
"LABEL_4X6_300",
- "LABEL_4X6_600"
+ "LABEL_4X6_600",
+ "LABEL_4X2_203",
+ "LABEL_4X2_300",
+ "LABEL_4X2_600",
+ "LABEL_4X1_203",
+ "LABEL_4X1_300",
+ "LABEL_4X1_600"
]
},
"DeviceRetryRequest": {
diff --git a/tools/bundle.mjs b/tools/bundle.mjs
index bfedbc6..2bad3aa 100644
--- a/tools/bundle.mjs
+++ b/tools/bundle.mjs
@@ -1,25 +1,29 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-import assert from 'node:assert/strict';
-import { createHash } from 'node:crypto';
-import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
-import { spawnSync } from 'node:child_process';
-import { fileURLToPath } from 'node:url';
+import assert from "node:assert/strict";
+import { createHash } from "node:crypto";
+import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
-const root = fileURLToPath(new URL('../', import.meta.url));
-const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
-assert(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(manifest.version), 'Invalid release version.');
-if(process.env.GITHUB_REF_TYPE === 'tag')
+const root = fileURLToPath(new URL("../", import.meta.url));
+/** @type {{version:string}} */
+const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
+assert(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(manifest.version), "Invalid release version.");
+if(process.env["GITHUB_REF_TYPE"] === "tag")
{
- assert.equal(process.env.GITHUB_REF_NAME, `v${manifest.version}`, 'Release tag must match package.json.');
+ assert.equal(process.env["GITHUB_REF_NAME"], `v${manifest.version}`, "Release tag must match package.json.");
}
-mkdirSync(new URL('../dist/', import.meta.url), { recursive: true });
+mkdirSync(new URL("../dist/", import.meta.url), { recursive: true });
const name = `qqq-print-protocol-${manifest.version}.tar.gz`;
-const files = ['README.md', 'LICENSE', 'NOTICE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md', 'package.json', 'package-lock.json', 'schemas', 'fixtures', 'tools', 'docs/INTEGRATION.md', 'docs/DEVICE-CONTROLS.md', 'docs/QQQ-AUTHENTICATION.md', 'docs/REALTIME.md', 'docs/PRINTER-MEDIA.md'];
-const result = spawnSync('tar', ['-czf', `dist/${name}`, ...files], { cwd: root, stdio: 'inherit', shell: false });
-if(result.error) throw result.error;
-assert.equal(result.status, 0, 'Archive creation failed.');
-const digest = createHash('sha256').update(readFileSync(new URL(`../dist/${name}`, import.meta.url))).digest('hex');
-writeFileSync(new URL('../dist/SHA256SUMS', import.meta.url), `${digest} ${name}\n`);
+const files = ["README.md", "LICENSE", "NOTICE", "CHANGELOG.md", "CONTRIBUTING.md", "SECURITY.md", "package.json", "package-lock.json", "tsconfig.json", "eslint.config.mjs", "schemas", "fixtures", "tools", "docs/INTEGRATION.md", "docs/DEVICE-CONTROLS.md", "docs/QQQ-AUTHENTICATION.md", "docs/REALTIME.md", "docs/PRINTER-MEDIA.md"];
+const result = spawnSync("tar", ["-czf", `dist/${name}`, ...files], { cwd: root, stdio: "inherit", shell: false });
+if(result.error)
+{
+ throw result.error;
+}
+assert.equal(result.status, 0, "Archive creation failed.");
+const digest = createHash("sha256").update(readFileSync(new URL(`../dist/${name}`, import.meta.url))).digest("hex");
+writeFileSync(new URL("../dist/SHA256SUMS", import.meta.url), `${digest} ${name}\n`);
console.log(`Created dist/${name} and dist/SHA256SUMS.`);
diff --git a/tools/create-content-fixtures.mjs b/tools/create-content-fixtures.mjs
index a96558c..9f4f6fe 100644
--- a/tools/create-content-fixtures.mjs
+++ b/tools/create-content-fixtures.mjs
@@ -1,35 +1,35 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-import { createHash } from 'node:crypto';
-import { writeFileSync } from 'node:fs';
+import { createHash } from "node:crypto";
+import { writeFileSync } from "node:fs";
// Keep printable fixtures synthetic and independent of a printer's settings.
-const content = Buffer.from('^XA\n^FO40,40^A0N,36,36^FDQQQ cloud print test^FS\n^FO40,90^A0N,24,24^FDProtocol v1 - one test label^FS\n^XZ\n', 'utf8');
-const root = new URL('../fixtures/', import.meta.url);
-writeFileSync(new URL('test-label.zpl', root), content);
+const content = Buffer.from("^XA\n^FO40,40^A0N,36,36^FDQQQ cloud print test^FS\n^FO40,90^A0N,24,24^FDProtocol v1 - one test label^FS\n^XZ\n", "utf8");
+const root = new URL("../fixtures/", import.meta.url);
+writeFileSync(new URL("test-label.zpl", root), content);
const job = {
- jobId: 'job-example',
- deviceId: 'device-example',
- printerId: 'zebra-example',
- title: 'QQQ cloud print test',
- format: 'ZPL',
- contentSha256: createHash('sha256').update(content).digest('hex'),
+ jobId: "job-example",
+ deviceId: "device-example",
+ printerId: "zebra-example",
+ title: "QQQ cloud print test",
+ format: "ZPL",
+ contentSha256: createHash("sha256").update(content).digest("hex"),
contentLength: content.length,
- options: { copies: 1, scale: 'ACTUAL_SIZE' },
- createdAt: '2026-10-29T18:00:00Z',
- status: 'CLAIMED'
+ options: { copies: 1, scale: "ACTUAL_SIZE" },
+ createdAt: "2026-10-29T18:00:00Z",
+ status: "CLAIMED",
};
const request = {
deviceId: job.deviceId,
printerId: job.printerId,
title: job.title,
format: job.format,
- contentBase64: content.toString('base64'),
- idempotencyKey: 'protocol-fixture-label-v1',
- options: job.options
+ contentBase64: content.toString("base64"),
+ idempotencyKey: "protocol-fixture-label-v1",
+ options: job.options,
};
-writeFileSync(new URL('submit.request.json', root), JSON.stringify(request, null, 2) + '\n');
-writeFileSync(new URL('claim.response.json', root), JSON.stringify({ job }, null, 2) + '\n');
-writeFileSync(new URL('job.response.json', root), JSON.stringify(job, null, 2) + '\n');
+writeFileSync(new URL("submit.request.json", root), JSON.stringify(request, null, 2) + "\n");
+writeFileSync(new URL("claim.response.json", root), JSON.stringify({ job }, null, 2) + "\n");
+writeFileSync(new URL("job.response.json", root), JSON.stringify(job, null, 2) + "\n");
diff --git a/tools/fixture-types.mjs b/tools/fixture-types.mjs
index 89703ca..5821a3c 100644
--- a/tools/fixture-types.mjs
+++ b/tools/fixture-types.mjs
@@ -3,82 +3,83 @@
// Shared correspondence between wire examples and schema definitions.
export const fixtureTypes = {
- 'printing-realtime-extension.discovery.json': 'PrintingRealtimeExtensionDiscovery',
- 'version-legacy.response.json': 'ProtocolVersionResponse',
- 'version-controls.response.json': 'ProtocolVersionResponse',
- 'control-poll.request.json': 'DeviceControlPollRequest',
- 'control-poll.response.json': 'DeviceControlPollResponse',
- 'control-disconnected.response.json': 'DeviceControlPollResponse',
- 'device-control.response.json': 'Device',
- 'action-pause-dispatch.request.json': 'AdminDeviceActionRequest',
- 'action-resume-dispatch.request.json': 'AdminDeviceActionRequest',
- 'action-disconnect.request.json': 'AdminDeviceActionRequest',
- 'action-reconnect.request.json': 'AdminDeviceActionRequest',
- 'action-revoke.request.json': 'AdminDeviceActionRequest',
- 'action-refresh-inventory.request.json': 'AdminDeviceActionRequest',
- 'action-clear-queue.request.json': 'AdminDeviceActionRequest',
- 'action-cancel-spool.request.json': 'AdminDeviceActionRequest',
- 'action-refresh.response.json': 'DeviceActionRecord',
- 'action-cancel.response.json': 'DeviceActionRecord',
- 'action-clear-queue.response.json': 'DeviceActionRecord',
- 'action-pause-dispatch.response.json': 'DeviceActionRecord',
- 'action-revoke.response.json': 'DeviceActionRecord',
- 'control-inventory-refreshed.receipt.json': 'DeviceCommandReceipt',
- 'control-cancel-requested.receipt.json': 'DeviceCommandReceipt',
- 'control-already-absent.receipt.json': 'DeviceCommandReceipt',
- 'control-ownership-mismatch.receipt.json': 'DeviceCommandReceipt',
- 'control-unsupported.receipt.json': 'DeviceCommandReceipt',
- 'control-failed.receipt.json': 'DeviceCommandReceipt',
- 'control-expired.receipt.json': 'DeviceCommandReceipt',
- 'control-outcome-unknown.receipt.json': 'DeviceCommandReceipt',
- 'control-commands-page.response.json': 'DeviceActionList',
- 'control-cancel-completed.response.json': 'DeviceActionRecord',
- 'heartbeat-profile.request.json': 'HeartbeatRequest',
- 'printers-profile.request.json': 'PrinterInventory',
- 'device-test.request.json': 'DeviceTestPrintRequest',
- 'device-test-profile.request.json': 'DeviceTestPrintRequest',
- 'device-retry.request.json': 'DeviceRetryRequest',
- 'create-enrollment.request.json': 'CreateEnrollmentRequest',
- 'enrollment.response.json': 'EnrollmentCode',
- 'enroll.request.json': 'EnrollRequest',
- 'enroll.response.json': 'EnrollResponse',
- 'heartbeat.request.json': 'HeartbeatRequest',
- 'heartbeat.response.json': 'HeartbeatResponse',
- 'printers.request.json': 'PrinterInventory',
- 'printers-page.response.json': 'PrinterList',
- 'approval.request.json': 'PrinterApproval',
- 'claim-empty.response.json': 'ClaimResponse',
- 'claim.response.json': 'ClaimResponse',
- 'job.response.json': 'PrintJob',
- 'submit.request.json': 'SubmitJobRequest',
- 'received.event.json': 'JobEvent',
- 'submitting.event.json': 'JobEvent',
- 'spooled.event.json': 'JobEvent',
- 'ack.response.json': 'Acknowledgment',
- 'error.response.json': 'Error',
- 'printing-session.response.json': 'PrintingSessionIdentity',
- 'printing-session-bound.response.json': 'PrintingSessionIdentity',
- 'realtime-settings.payload.json': 'RealtimeSettings',
- 'realtime-hello.message.json': 'RealtimeHello',
- 'realtime-welcome.message.json': 'RealtimeWelcome',
- 'realtime-state-applied.message.json': 'RealtimeStateApplied',
- 'realtime-desired-state.message.json': 'RealtimeDesiredState',
- 'realtime-job-available.message.json': 'RealtimeJobAvailable',
- 'realtime-command.message.json': 'RealtimeCommand',
- 'realtime-command-receipt.message.json': 'RealtimeCommandReceipt',
- 'realtime-job-event.message.json': 'RealtimeJobEvent',
- 'realtime-job-event-replay.message.json': 'RealtimeJobEvent',
- 'realtime-settings.message.json': 'RealtimeSettingsMessage',
- 'realtime-ack.message.json': 'RealtimeAck',
- 'realtime-error.message.json': 'RealtimeError',
- 'realtime-heartbeat.message.json': 'RealtimeHeartbeat',
- 'realtime-inventory.message.json': 'RealtimeInventory',
- 'realtime-ping.message.json': 'RealtimePing',
- 'realtime-pong.message.json': 'RealtimePong',
- 'realtime-large-pdf.response.json': 'PrintJob',
- 'printer-media-profile.response.json': 'PrinterMediaProfile',
- 'media-profile-options.request.json': 'PrintOptions',
- 'operator-printers.response.json': 'OperatorPrinterList',
- 'operator-submit.request.json': 'OperatorSubmitJobRequest',
- 'realtime-media-hello.message.json': 'RealtimeHello'
+ "printing-realtime-extension.discovery.json": "PrintingRealtimeExtensionDiscovery",
+ "version-legacy.response.json": "ProtocolVersionResponse",
+ "version-controls.response.json": "ProtocolVersionResponse",
+ "control-poll.request.json": "DeviceControlPollRequest",
+ "control-poll.response.json": "DeviceControlPollResponse",
+ "control-disconnected.response.json": "DeviceControlPollResponse",
+ "device-control.response.json": "Device",
+ "action-pause-dispatch.request.json": "AdminDeviceActionRequest",
+ "action-resume-dispatch.request.json": "AdminDeviceActionRequest",
+ "action-disconnect.request.json": "AdminDeviceActionRequest",
+ "action-reconnect.request.json": "AdminDeviceActionRequest",
+ "action-revoke.request.json": "AdminDeviceActionRequest",
+ "action-refresh-inventory.request.json": "AdminDeviceActionRequest",
+ "action-clear-queue.request.json": "AdminDeviceActionRequest",
+ "action-cancel-spool.request.json": "AdminDeviceActionRequest",
+ "action-refresh.response.json": "DeviceActionRecord",
+ "action-cancel.response.json": "DeviceActionRecord",
+ "action-clear-queue.response.json": "DeviceActionRecord",
+ "action-pause-dispatch.response.json": "DeviceActionRecord",
+ "action-revoke.response.json": "DeviceActionRecord",
+ "control-inventory-refreshed.receipt.json": "DeviceCommandReceipt",
+ "control-cancel-requested.receipt.json": "DeviceCommandReceipt",
+ "control-already-absent.receipt.json": "DeviceCommandReceipt",
+ "control-ownership-mismatch.receipt.json": "DeviceCommandReceipt",
+ "control-unsupported.receipt.json": "DeviceCommandReceipt",
+ "control-failed.receipt.json": "DeviceCommandReceipt",
+ "control-expired.receipt.json": "DeviceCommandReceipt",
+ "control-outcome-unknown.receipt.json": "DeviceCommandReceipt",
+ "control-commands-page.response.json": "DeviceActionList",
+ "control-cancel-completed.response.json": "DeviceActionRecord",
+ "heartbeat-profile.request.json": "HeartbeatRequest",
+ "printers-profile.request.json": "PrinterInventory",
+ "device-test.request.json": "DeviceTestPrintRequest",
+ "device-test-profile.request.json": "DeviceTestPrintRequest",
+ "device-test-profile-4x1.request.json": "DeviceTestPrintRequest",
+ "device-retry.request.json": "DeviceRetryRequest",
+ "create-enrollment.request.json": "CreateEnrollmentRequest",
+ "enrollment.response.json": "EnrollmentCode",
+ "enroll.request.json": "EnrollRequest",
+ "enroll.response.json": "EnrollResponse",
+ "heartbeat.request.json": "HeartbeatRequest",
+ "heartbeat.response.json": "HeartbeatResponse",
+ "printers.request.json": "PrinterInventory",
+ "printers-page.response.json": "PrinterList",
+ "approval.request.json": "PrinterApproval",
+ "claim-empty.response.json": "ClaimResponse",
+ "claim.response.json": "ClaimResponse",
+ "job.response.json": "PrintJob",
+ "submit.request.json": "SubmitJobRequest",
+ "received.event.json": "JobEvent",
+ "submitting.event.json": "JobEvent",
+ "spooled.event.json": "JobEvent",
+ "ack.response.json": "Acknowledgment",
+ "error.response.json": "Error",
+ "printing-session.response.json": "PrintingSessionIdentity",
+ "printing-session-bound.response.json": "PrintingSessionIdentity",
+ "realtime-settings.payload.json": "RealtimeSettings",
+ "realtime-hello.message.json": "RealtimeHello",
+ "realtime-welcome.message.json": "RealtimeWelcome",
+ "realtime-state-applied.message.json": "RealtimeStateApplied",
+ "realtime-desired-state.message.json": "RealtimeDesiredState",
+ "realtime-job-available.message.json": "RealtimeJobAvailable",
+ "realtime-command.message.json": "RealtimeCommand",
+ "realtime-command-receipt.message.json": "RealtimeCommandReceipt",
+ "realtime-job-event.message.json": "RealtimeJobEvent",
+ "realtime-job-event-replay.message.json": "RealtimeJobEvent",
+ "realtime-settings.message.json": "RealtimeSettingsMessage",
+ "realtime-ack.message.json": "RealtimeAck",
+ "realtime-error.message.json": "RealtimeError",
+ "realtime-heartbeat.message.json": "RealtimeHeartbeat",
+ "realtime-inventory.message.json": "RealtimeInventory",
+ "realtime-ping.message.json": "RealtimePing",
+ "realtime-pong.message.json": "RealtimePong",
+ "realtime-large-pdf.response.json": "PrintJob",
+ "printer-media-profile.response.json": "PrinterMediaProfile",
+ "media-profile-options.request.json": "PrintOptions",
+ "operator-printers.response.json": "OperatorPrinterList",
+ "operator-submit.request.json": "OperatorSubmitJobRequest",
+ "realtime-media-hello.message.json": "RealtimeHello",
};
diff --git a/tools/verify-fixtures.mjs b/tools/verify-fixtures.mjs
index 1b5ec09..d378b60 100644
--- a/tools/verify-fixtures.mjs
+++ b/tools/verify-fixtures.mjs
@@ -1,148 +1,245 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-import assert from 'node:assert/strict';
-import { createHash } from 'node:crypto';
-import { readFileSync } from 'node:fs';
-import { fixtureTypes } from './fixture-types.mjs';
+import assert from "node:assert/strict";
+import { createHash } from "node:crypto";
+import { readFileSync } from "node:fs";
+import { fixtureTypes } from "./fixture-types.mjs";
-const schema = JSON.parse(readFileSync(new URL('../schemas/protocol.schema.json', import.meta.url), 'utf8'));
-const root = new URL('../fixtures/', import.meta.url);
+/** @typedef {{ $ref?:string,not?:Rule|boolean,required?:string[],type?:string,anyOf?:(Rule|boolean)[],const?:unknown,enum?:unknown[],additionalProperties?:boolean,properties?:Record,dependentRequired?:Record,minItems?:number,maxItems?:number,uniqueItems?:boolean,contains?:Rule|boolean,items?:Rule|boolean,minLength?:number,maxLength?:number,pattern?:string,format?:string,contentEncoding?:string,minimum?:number,maximum?:number,exclusiveMinimum?:number }} Rule */
+/** @type {{$defs:Record}} */
+const schema = JSON.parse(readFileSync(new URL("../schemas/protocol.schema.json", import.meta.url), "utf8"));
+const root = new URL("../fixtures/", import.meta.url);
// Validate the schema keywords used by this contract without installing dependencies.
+/** @param {unknown} value @param {Rule|boolean} rule @param {string} path @returns {void} */
function validate(value, rule, path)
{
- if(typeof rule === 'boolean')
+ if(typeof rule === "boolean")
{
assert(rule, `${path}: forbidden property`);
return;
}
if(rule.$ref)
{
- return validate(value, schema.$defs[rule.$ref.split('/').at(-1)], path);
+ return validate(value, /** @type {Rule} */ (schema.$defs[/** @type {string} */ (rule.$ref.split("/").at(-1))]), path);
}
if(rule.not)
{
let matches = true;
- try { validate(value, rule.not, path); }
- catch { matches = false; }
+ try
+ {
+ validate(value, rule.not, path);
+ }
+ catch
+ {
+ matches = false;
+ }
assert(!matches, `${path}: forbidden combination`);
}
if(rule.required && !rule.type)
{
- for(const name of rule.required) assert(name in value, `${path}.${name}: required`);
+ for(const name of rule.required)
+ {
+ assert(name in /** @type {object} */ (value), `${path}.${name}: required`);
+ }
}
if(rule.anyOf)
{
assert(rule.anyOf.some(candidate =>
{
- try { validate(value, candidate, path); return true; }
- catch { return false; }
+ try
+ {
+ validate(value, candidate, path); return true;
+ }
+ catch
+ {
+ return false;
+ }
}), `${path}: no anyOf variant matched`);
}
- if(rule.const !== undefined) assert.deepEqual(value, rule.const, path);
- if(rule.enum) assert(rule.enum.includes(value), `${path}: invalid enum`);
- if(rule.type === 'null') assert.equal(value, null, path);
- if(rule.type === 'object')
+ if(rule.const !== undefined)
{
- assert(value !== null && typeof value === 'object' && !Array.isArray(value), `${path}: expected object`);
- for(const name of rule.required ?? []) assert(name in value, `${path}.${name}: required`);
+ assert.deepEqual(value, rule.const, path);
+ }
+ if(rule.enum)
+ {
+ assert(rule.enum.includes(value), `${path}: invalid enum`);
+ }
+ if(rule.type === "null")
+ {
+ assert.equal(value, null, path);
+ }
+ if(rule.type === "object")
+ {
+ assert(value !== null && typeof value === "object" && !Array.isArray(value), `${path}: expected object`);
+ for(const name of rule.required ?? [])
+ {
+ assert(name in /** @type {object} */ (value), `${path}.${name}: required`);
+ }
if(rule.additionalProperties === false)
{
- for(const name of Object.keys(value)) assert(name in (rule.properties ?? {}), `${path}.${name}: unknown property`);
+ for(const name of Object.keys(value))
+ {
+ assert(name in (rule.properties ?? {}), `${path}.${name}: unknown property`);
+ }
}
for(const [name, child] of Object.entries(rule.properties ?? {}))
{
- if(name in value) validate(value[name], child, `${path}.${name}`);
+ if(name in /** @type {object} */ (value))
+ {
+ validate(/** @type {Record} */ (value)[name], child, `${path}.${name}`);
+ }
}
for(const [name, dependencies] of Object.entries(rule.dependentRequired ?? {}))
{
- if(name in value) for(const dependency of dependencies) assert(dependency in value, `${path}.${dependency}: required with ${name}`);
+ if(name in /** @type {object} */ (value))
+ {
+ for(const dependency of dependencies)
+ {
+ assert(dependency in value, `${path}.${dependency}: required with ${name}`);
+ }
+ }
}
}
- if(rule.type === 'array')
+ if(rule.type === "array")
{
assert(Array.isArray(value), `${path}: expected array`);
- if(rule.minItems !== undefined) assert(value.length >= rule.minItems, path);
- if(rule.maxItems !== undefined) assert(value.length <= rule.maxItems, path);
- if(rule.uniqueItems) assert.equal(new Set(value.map(item => JSON.stringify(item))).size, value.length, path);
+ if(rule.minItems !== undefined)
+ {
+ assert(value.length >= rule.minItems, path);
+ }
+ if(rule.maxItems !== undefined)
+ {
+ assert(value.length <= rule.maxItems, path);
+ }
+ if(rule.uniqueItems)
+ {
+ assert.equal(new Set(value.map(item => JSON.stringify(item))).size, value.length, path);
+ }
if(rule.contains)
{
assert(value.some(item =>
{
- try { validate(item, rule.contains, path); return true; }
- catch { return false; }
+ try
+ {
+ validate(item, /** @type {Rule|boolean} */ (rule.contains), path); return true;
+ }
+ catch
+ {
+ return false;
+ }
}), `${path}: required array member missing`);
}
- value.forEach((item, index) => validate(item, rule.items, `${path}[${index}]`));
+ value.forEach((item, index) => validate(item, /** @type {Rule|boolean} */ (rule.items), `${path}[${index}]`));
}
- if(rule.type === 'string')
+ if(rule.type === "string")
{
- assert.equal(typeof value, 'string', path);
- if(rule.minLength !== undefined) assert(value.length >= rule.minLength, path);
- if(rule.maxLength !== undefined) assert(value.length <= rule.maxLength, path);
- if(rule.pattern) assert(new RegExp(rule.pattern).test(value), path);
- if(rule.format === 'date-time') assert(!Number.isNaN(Date.parse(value)) && value.endsWith('Z'), `${path}: UTC date-time required`);
- if(rule.contentEncoding === 'base64') assert.equal(Buffer.from(value, 'base64').toString('base64'), value, path);
+ assert.equal(typeof value, "string", path);
+ if(rule.minLength !== undefined)
+ {
+ assert((/** @type {string} */ (value)).length >= rule.minLength, path);
+ }
+ if(rule.maxLength !== undefined)
+ {
+ assert((/** @type {string} */ (value)).length <= rule.maxLength, path);
+ }
+ if(rule.pattern)
+ {
+ assert(new RegExp(rule.pattern).test(/** @type {string} */ (value)), path);
+ }
+ if(rule.format === "date-time")
+ {
+ assert(!Number.isNaN(Date.parse(/** @type {string} */ (value))) && (/** @type {string} */ (value)).endsWith("Z"), `${path}: UTC date-time required`);
+ }
+ if(rule.contentEncoding === "base64")
+ {
+ assert.equal(Buffer.from(/** @type {string} */ (value), "base64").toString("base64"), value, path);
+ }
}
- if(rule.type === 'boolean') assert.equal(typeof value, 'boolean', path);
- if(rule.type === 'integer' || rule.type === 'number')
+ if(rule.type === "boolean")
{
- assert.equal(typeof value, 'number', path);
+ assert.equal(typeof value, "boolean", path);
+ }
+ if(rule.type === "integer" || rule.type === "number")
+ {
+ assert.equal(typeof value, "number", path);
assert(Number.isFinite(value), path);
- if(rule.type === 'integer') assert(Number.isInteger(value), path);
- if(rule.minimum !== undefined) assert(value >= rule.minimum, path);
- if(rule.maximum !== undefined) assert(value <= rule.maximum, path);
- if(rule.exclusiveMinimum !== undefined) assert(value > rule.exclusiveMinimum, path);
+ if(rule.type === "integer")
+ {
+ assert(Number.isInteger(value), path);
+ }
+ if(rule.minimum !== undefined)
+ {
+ assert(/** @type {number} */ (value) >= rule.minimum, path);
+ }
+ if(rule.maximum !== undefined)
+ {
+ assert(/** @type {number} */ (value) <= rule.maximum, path);
+ }
+ if(rule.exclusiveMinimum !== undefined)
+ {
+ assert(/** @type {number} */ (value) > rule.exclusiveMinimum, path);
+ }
}
}
+/** @type {Record} */
const fixtures = {};
for(const [name, type] of Object.entries(fixtureTypes))
{
- fixtures[name] = JSON.parse(readFileSync(new URL(name, root), 'utf8'));
- validate(fixtures[name], schema.$defs[type], name);
+ fixtures[name] = JSON.parse(readFileSync(new URL(name, root), "utf8"));
+ validate(fixtures[name], /** @type {Rule} */ (schema.$defs[type]), name);
}
-const content = readFileSync(new URL('test-label.zpl', root));
-const job = fixtures['claim.response.json'].job;
+const content = readFileSync(new URL("test-label.zpl", root));
+const job = (/** @type {typeof import("../fixtures/claim.response.json")} */ (fixtures["claim.response.json"])).job;
assert.equal(content.length, job.contentLength);
-assert.equal(createHash('sha256').update(content).digest('hex'), job.contentSha256);
-assert.deepEqual(Buffer.from(fixtures['submit.request.json'].contentBase64, 'base64'), content);
-assert.deepEqual(fixtures['job.response.json'], job);
-assert.throws(() => validate({ copies: 0 }, schema.$defs.PrintOptions, 'invalidCopies'));
-assert.throws(() => validate({ paperWidthMm: 100 }, schema.$defs.PrintOptions, 'missingHeight'));
-assert.throws(() => validate({ eventId: 'test', status: 'PRINTED' }, schema.$defs.JobEvent, 'invalidStatus'));
-assert.throws(() => validate({ name: 'x'.repeat(513) }, schema.$defs.DeviceProfile, 'oversizedName'));
-assert.throws(() => validate({ description: 'x'.repeat(4097) }, schema.$defs.DeviceProfile, 'oversizedDescription'));
-assert.throws(() => validate({ location: 'x'.repeat(513) }, schema.$defs.DeviceProfile, 'oversizedLocation'));
-validate({ name: '', description: null }, schema.$defs.DeviceProfile, 'clearAndPreserve');
-const brandedTest = fixtures['device-test-profile.request.json'];
-assert.throws(() => validate({ ...brandedTest, testProfile: 'LETTER' }, schema.$defs.DeviceTestPrintRequest, 'incompatibleLabelMedia'));
-assert.throws(() => validate({ ...brandedTest, testProfile: 'UNKNOWN' }, schema.$defs.DeviceTestPrintRequest, 'unknownTestProfile'));
-assert.throws(() => validate({ ...brandedTest, testProfile: null }, schema.$defs.DeviceTestPrintRequest, 'nullTestProfile'));
-assert.throws(() => validate({ format: 'PDF' }, schema.$defs.DeviceTestPrintRequest, 'missingTestIdentity'));
-validate({ ...brandedTest, format: 'PDF', testProfile: 'A4' }, schema.$defs.DeviceTestPrintRequest, 'a4TestPage');
-const clear = fixtures['action-clear-queue.request.json'];
-assert.throws(() => validate({ ...clear, issuedAt: undefined }, schema.$defs.AdminDeviceActionRequest, 'missingIssuedAt'));
-assert.throws(() => validate({ ...clear, jobId: 'job-example-1' }, schema.$defs.AdminDeviceActionRequest, 'clearCannotTargetSpool'));
-assert.throws(() => validate({ ...clear, action: 'RESTART_SERVICE' }, schema.$defs.AdminDeviceActionRequest, 'arbitraryServiceControl'));
-assert.throws(() => validate({ ...clear, command: 'shell' }, schema.$defs.AdminDeviceActionRequest, 'unknownCommandProperty'));
-assert.throws(() => validate({ ...clear, action: 'CANCEL_SPOOL' }, schema.$defs.AdminDeviceActionRequest, 'cancelRequiresJobIdentity'));
-assert.throws(() => validate({ ...fixtures['control-poll.request.json'], capabilities: ['CANCEL_SPOOL', 'CANCEL_SPOOL'] }, schema.$defs.DeviceControlPollRequest, 'duplicateCapability'));
-assert.throws(() => validate({ ...fixtures['control-cancel-requested.receipt.json'], outcome: 'PRINT_RECALLED' }, schema.$defs.DeviceCommandReceipt, 'unsupportedPaperClaim'));
+assert.equal(createHash("sha256").update(content).digest("hex"), job.contentSha256);
+assert.deepEqual(Buffer.from((/** @type {typeof import("../fixtures/submit.request.json")} */ (fixtures["submit.request.json"])).contentBase64, "base64"), content);
+assert.deepEqual(fixtures["job.response.json"], job);
+assert.throws(() => validate({ copies: 0 }, /** @type {Rule} */ (schema.$defs["PrintOptions"]), "invalidCopies"));
+assert.throws(() => validate({ paperWidthMm: 100 }, /** @type {Rule} */ (schema.$defs["PrintOptions"]), "missingHeight"));
+assert.throws(() => validate({ eventId: "test", status: "PRINTED" }, /** @type {Rule} */ (schema.$defs["JobEvent"]), "invalidStatus"));
+assert.throws(() => validate({ name: "x".repeat(513) }, /** @type {Rule} */ (schema.$defs["DeviceProfile"]), "oversizedName"));
+assert.throws(() => validate({ description: "x".repeat(4097) }, /** @type {Rule} */ (schema.$defs["DeviceProfile"]), "oversizedDescription"));
+assert.throws(() => validate({ location: "x".repeat(513) }, /** @type {Rule} */ (schema.$defs["DeviceProfile"]), "oversizedLocation"));
+validate({ name: "", description: null }, /** @type {Rule} */ (schema.$defs["DeviceProfile"]), "clearAndPreserve");
+const brandedTest = fixtures["device-test-profile.request.json"];
+assert.throws(() => validate({ ...brandedTest, testProfile: "LETTER" }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), "incompatibleLabelMedia"));
+assert.throws(() => validate({ ...brandedTest, testProfile: "UNKNOWN" }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), "unknownTestProfile"));
+assert.throws(() => validate({ ...brandedTest, testProfile: null }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), "nullTestProfile"));
+assert.throws(() => validate({ format: "PDF" }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), "missingTestIdentity"));
+validate({ ...brandedTest, format: "PDF", testProfile: "A4" }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), "a4TestPage");
+for(const testProfile of ["LABEL_4X2_203", "LABEL_4X2_300", "LABEL_4X2_600", "LABEL_4X1_203", "LABEL_4X1_300", "LABEL_4X1_600"])
+{
+ validate({ ...brandedTest, testProfile }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), `zplShortLabel${testProfile}`);
+ validate({ ...brandedTest, format: "PDF", testProfile }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), `pdfShortLabel${testProfile}`);
+}
+assert.throws(() => validate({ ...brandedTest, testProfile: "LABEL_4X3_300" }, /** @type {Rule} */ (schema.$defs["DeviceTestPrintRequest"]), "unsupportedLabelHeight"));
+// Short PDF labels use explicit actual-size job paper, never a conflicting media selection.
+validate({ copies: 1, scale: "ACTUAL_SIZE", paperWidthMm: 101.6, paperHeightMm: 25.4 }, /** @type {Rule} */ (schema.$defs["PrintOptions"]), "fourByOneJobPaper");
+validate({ copies: 1, scale: "ACTUAL_SIZE", paperWidthMm: 101.6, paperHeightMm: 50.8 }, /** @type {Rule} */ (schema.$defs["PrintOptions"]), "fourByTwoJobPaper");
+const clear = fixtures["action-clear-queue.request.json"];
+assert.throws(() => validate({ ...clear, issuedAt: undefined }, /** @type {Rule} */ (schema.$defs["AdminDeviceActionRequest"]), "missingIssuedAt"));
+assert.throws(() => validate({ ...clear, jobId: "job-example-1" }, /** @type {Rule} */ (schema.$defs["AdminDeviceActionRequest"]), "clearCannotTargetSpool"));
+assert.throws(() => validate({ ...clear, action: "RESTART_SERVICE" }, /** @type {Rule} */ (schema.$defs["AdminDeviceActionRequest"]), "arbitraryServiceControl"));
+assert.throws(() => validate({ ...clear, command: "shell" }, /** @type {Rule} */ (schema.$defs["AdminDeviceActionRequest"]), "unknownCommandProperty"));
+assert.throws(() => validate({ ...clear, action: "CANCEL_SPOOL" }, /** @type {Rule} */ (schema.$defs["AdminDeviceActionRequest"]), "cancelRequiresJobIdentity"));
+assert.throws(() => validate({ ...fixtures["control-poll.request.json"], capabilities: ["CANCEL_SPOOL", "CANCEL_SPOOL"] }, /** @type {Rule} */ (schema.$defs["DeviceControlPollRequest"]), "duplicateCapability"));
+assert.throws(() => validate({ ...fixtures["control-cancel-requested.receipt.json"], outcome: "PRINT_RECALLED" }, /** @type {Rule} */ (schema.$defs["DeviceCommandReceipt"]), "unsupportedPaperClaim"));
console.log(`Validated ${Object.keys(fixtures).length} protocol fixtures, content integrity, and invalid-input checks.`);
-const media = fixtures['printer-media-profile.response.json'];
-for(const invalid of [{ dpiX: 0 }, { widthMicrons: 0 }, { revision: 'not-a-revision' }, { applyMode: 'CHANGE_STOCK' }])
+const media = fixtures["printer-media-profile.response.json"];
+for(const invalid of [{ dpiX: 0 }, { widthMicrons: 0 }, { revision: "not-a-revision" }, { applyMode: "CHANGE_STOCK" }])
{
- assert.throws(() => validate({ ...media, ...invalid }, schema.$defs.PrinterMediaProfile, 'invalidMedia'));
+ assert.throws(() => validate({ ...media, ...invalid }, /** @type {Rule} */ (schema.$defs["PrinterMediaProfile"]), "invalidMedia"));
}
-const selection = fixtures['media-profile-options.request.json'];
-assert.throws(() => validate({ ...selection, paperWidthMm: 100, paperHeightMm: 150 }, schema.$defs.PrintOptions, 'ambiguousMedia'));
+const selection = fixtures["media-profile-options.request.json"];
+assert.throws(() => validate({ ...selection, paperWidthMm: 100, paperHeightMm: 150 }, /** @type {Rule} */ (schema.$defs["PrintOptions"]), "ambiguousMedia"));
-const operatorRequest = fixtures['operator-submit.request.json'];
-for(const invalid of [{ requestId: 'x'.repeat(129) }, { requestId: 'echo/scan' }, { actorId: 'forged' }, { tenantId: 'forged' }])
+const operatorRequest = fixtures["operator-submit.request.json"];
+for(const invalid of [{ requestId: "x".repeat(129) }, { requestId: "echo/scan" }, { actorId: "forged" }, { tenantId: "forged" }])
{
- assert.throws(() => validate({ ...operatorRequest, ...invalid }, schema.$defs.OperatorSubmitJobRequest, 'invalidOperatorRequest'));
+ assert.throws(() => validate({ ...operatorRequest, ...invalid }, /** @type {Rule} */ (schema.$defs["OperatorSubmitJobRequest"]), "invalidOperatorRequest"));
}
-validate({ ...operatorRequest, requestId: 'x'.repeat(128) }, schema.$defs.OperatorSubmitJobRequest, 'maximumRequestKey');
+validate({ ...operatorRequest, requestId: "x".repeat(128) }, /** @type {Rule} */ (schema.$defs["OperatorSubmitJobRequest"]), "maximumRequestKey");
diff --git a/tools/verify-realtime.mjs b/tools/verify-realtime.mjs
index 52dfb12..4699a09 100644
--- a/tools/verify-realtime.mjs
+++ b/tools/verify-realtime.mjs
@@ -1,62 +1,72 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-import assert from 'node:assert/strict';
-import { readFileSync } from 'node:fs';
-import Ajv2020 from 'ajv/dist/2020.js';
-import addFormats from 'ajv-formats';
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { Ajv2020 } from "ajv/dist/2020.js";
+import addFormats from "ajv-formats";
-const schema = JSON.parse(readFileSync(new URL('../schemas/protocol.schema.json', import.meta.url), 'utf8'));
+const schema = JSON.parse(readFileSync(new URL("../schemas/protocol.schema.json", import.meta.url), "utf8"));
const ajv = new Ajv2020({ strict: true, allErrors: true });
-addFormats(ajv);
+// ajv-formats is callable CommonJS; its NodeNext declaration exposes a namespace.
+/** @type {import("ajv-formats").FormatsPlugin} */ (/** @type {unknown} */ (addFormats))(ajv);
ajv.addSchema(schema);
-const fixture = name => JSON.parse(readFileSync(new URL(`../fixtures/${name}`, import.meta.url), 'utf8'));
+/** @param {string} name @returns {Record} */
+const fixture = name => JSON.parse(readFileSync(new URL(`../fixtures/${name}`, import.meta.url), "utf8"));
+/** @param {string} type @param {unknown} value @returns {boolean|Promise} */
const valid = (type, value) => ajv.validate(`${schema.$id}#/$defs/${type}`, value);
-const settings = fixture('realtime-settings.payload.json');
-const hello = fixture('realtime-hello.message.json');
-const extension = fixture('printing-realtime-extension.discovery.json');
-assert(valid('PrintingRealtimeExtensionDiscovery', extension), ajv.errorsText());
-assert.equal(extension.version, '2.0.0');
-assert(valid('PrintingRealtimeExtensionDiscovery', { ...extension, version: '2.7.3' }));
-for(const version of ['1.0.0', '1.1.0', '3.0.0', '2.0', '2.01.0', '2.0.0-rc.1', null])
+const settings = /** @type {typeof import("../fixtures/realtime-settings.payload.json")} */ (/** @type {unknown} */ (fixture("realtime-settings.payload.json")));
+const hello = /** @type {typeof import("../fixtures/realtime-hello.message.json")} */ (/** @type {unknown} */ (fixture("realtime-hello.message.json")));
+const extension = fixture("printing-realtime-extension.discovery.json");
+assert(valid("PrintingRealtimeExtensionDiscovery", extension), ajv.errorsText());
+assert.equal(extension["version"], "2.0.0");
+assert(valid("PrintingRealtimeExtensionDiscovery", { ...extension, version: "2.7.3" }));
+for(const version of ["1.0.0", "1.1.0", "3.0.0", "2.0", "2.01.0", "2.0.0-rc.1", null])
{
- assert(!valid('PrintingRealtimeExtensionDiscovery', { ...extension, version }), 'Only the compatible stable native major is accepted.');
+ assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, version }), "Only the compatible stable native major is accepted.");
}
-assert(!valid('PrintingRealtimeExtensionDiscovery', { ...extension, name: 'another-extension' }));
-assert(!valid('PrintingRealtimeExtensionDiscovery', { ...extension, pathPrefix: '/printing/v1' }));
+assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, name: "another-extension" }));
+assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, pathPrefix: "/printing/v1" }));
/*******************************************************************************
** Cross-field policy relationships require semantic checks beyond JSON Schema.
*******************************************************************************/
+/** @param {typeof settings} policy @param {typeof hello.payload.limits} limits @returns {void} */
function assertPolicy(policy, limits)
{
- assert(valid('RealtimeSettings', policy), ajv.errorsText());
- assert(valid('RealtimeClientLimits', limits), ajv.errorsText());
- assert(policy.maxQueuedFrameBytes >= policy.maxFrameBytes, 'Frame queue must hold one permitted message.');
- assert(policy.maxStagedBytes >= Math.max(policy.maxPdfBytes, policy.maxZplBytes), 'Staging must admit one permitted document.');
+ assert(valid("RealtimeSettings", policy), ajv.errorsText());
+ assert(valid("RealtimeClientLimits", limits), ajv.errorsText());
+ assert(policy.maxQueuedFrameBytes >= policy.maxFrameBytes, "Frame queue must hold one permitted message.");
+ assert(policy.maxStagedBytes >= Math.max(policy.maxPdfBytes, policy.maxZplBytes), "Staging must admit one permitted document.");
assert(policy.downloadTimeoutMillis >= policy.downloadConnectTimeoutMillis);
assert(policy.downloadTimeoutMillis >= policy.downloadIdleTimeoutMillis);
assert(policy.heartbeatTimeoutSeconds >= 2 * policy.heartbeatIntervalSeconds);
assert(policy.reconnectMaxDelayMillis >= policy.reconnectMinDelayMillis);
- for(const name of schema.$defs.RealtimeClientLimits.required)
+ for(const name of /** @type {(keyof typeof hello.payload.limits)[]} */ (schema.$defs.RealtimeClientLimits.required))
{
assert(policy[name] <= limits[name], `${name}: policy exceeds declared client capability`);
}
}
assertPolicy(settings, hello.payload.limits);
-assert.equal(settings.maxConcurrentDownloads, 1, 'Initial deployment defaults to one configurable download lane.');
+assert.equal(settings.maxConcurrentDownloads, 1, "Initial deployment defaults to one configurable download lane.");
assert.equal(settings.maxCopies, 100);
assert.equal(settings.nativeSubmissionTimeoutMillis, 120000);
-for(const [name, rule] of Object.entries(schema.$defs.RealtimeSettings.properties))
+for(const [name, rule] of Object.entries(/** @type {Record} */ (schema.$defs.RealtimeSettings.properties)))
{
- if(rule.default !== undefined) assert.equal(settings[name], rule.default, `${name}: fixture/schema default mismatch`);
+ if(rule.default !== undefined)
+ {
+ assert.equal(settings[/** @type {keyof typeof settings} */ (name)], rule.default, `${name}: fixture/schema default mismatch`);
+ }
+ /** @type {Partial} */
const missing = { ...settings };
- delete missing[name];
- assert(!valid('RealtimeSettings', missing), `${name}: missing value cannot inherit a default`);
+ // Negative evidence deliberately removes each required schema field.
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete missing[/** @type {keyof typeof settings} */ (name)];
+ assert(!valid("RealtimeSettings", missing), `${name}: missing value cannot inherit a default`);
for(const value of [rule.minimum - 1, rule.maximum + 1, 1.5, null])
{
- assert(!valid('RealtimeSettings', { ...settings, [name]: value }), `${name}: invalid bound/type accepted`);
+ assert(!valid("RealtimeSettings", { ...settings, [name]: value }), `${name}: invalid bound/type accepted`);
}
}
for(const change of [
@@ -64,50 +74,50 @@ for(const change of [
{ maxStagedBytes: 1 },
{ downloadTimeoutMillis: 1000 },
{ heartbeatTimeoutSeconds: 10 },
- { reconnectMinDelayMillis: 60000, reconnectMaxDelayMillis: 1000 }
+ { reconnectMinDelayMillis: 60000, reconnectMaxDelayMillis: 1000 },
])
{
assert.throws(() => assertPolicy({ ...settings, ...change }, hello.payload.limits));
}
-for(const name of schema.$defs.RealtimeClientLimits.required)
+for(const name of /** @type {(keyof typeof hello.payload.limits)[]} */ (schema.$defs.RealtimeClientLimits.required))
{
- assert.throws(() => assertPolicy(settings, { ...hello.payload.limits, [name]: settings[name] - 1 }), name);
+ assert.throws(() => assertPolicy(settings, { ...hello.payload.limits, [name]: settings[/** @type {keyof typeof settings} */ (name)] - 1 }), name);
}
assertPolicy({ ...settings, maxConcurrentDownloads: 16, maxConcurrentPrints: 16, maxCopies: 32767 }, hello.payload.limits);
-assert(!valid('RealtimeHello', { ...hello, payload: { ...hello.payload, capabilities: [] } }));
-assert(!valid('RealtimeHello', { ...hello, payload: { ...hello.payload, capabilities: ['CANCEL_SPOOL'] } }));
-assert(!valid('RealtimeHello', { ...hello, payload: { ...hello.payload, limits: {} } }));
-assert(!valid('RealtimeHello', { ...hello, messageId: 'hello:unsafe-prefix' }));
-assert(!valid('RealtimeHello', { ...hello, token: 'not-a-real-token' }));
-assert(!valid('RealtimeClientMessage', { type: 'RUN_SHELL', messageId: 'unsupported-1', payload: {} }));
+assert(!valid("RealtimeHello", { ...hello, payload: { ...hello.payload, capabilities: [] } }));
+assert(!valid("RealtimeHello", { ...hello, payload: { ...hello.payload, capabilities: ["CANCEL_SPOOL"] } }));
+assert(!valid("RealtimeHello", { ...hello, payload: { ...hello.payload, limits: {} } }));
+assert(!valid("RealtimeHello", { ...hello, messageId: "hello:unsafe-prefix" }));
+assert(!valid("RealtimeHello", { ...hello, token: "not-a-real-token" }));
+assert(!valid("RealtimeClientMessage", { type: "RUN_SHELL", messageId: "unsupported-1", payload: {} }));
-const clientFixtures = ['hello', 'state-applied', 'job-event', 'command-receipt', 'heartbeat', 'inventory', 'ping'];
-const serverFixtures = ['welcome', 'desired-state', 'job-available', 'command', 'settings', 'ack', 'error', 'pong'];
-for(const [files, accepted, rejected] of [
- [clientFixtures, 'RealtimeClientMessage', 'RealtimeServerMessage'],
- [serverFixtures, 'RealtimeServerMessage', 'RealtimeClientMessage']
-])
+const clientFixtures = ["hello", "state-applied", "job-event", "command-receipt", "heartbeat", "inventory", "ping"];
+const serverFixtures = ["welcome", "desired-state", "job-available", "command", "settings", "ack", "error", "pong"];
+for(const [files, accepted, rejected] of /** @type {[string[],string,string][]} */ ([
+ [clientFixtures, "RealtimeClientMessage", "RealtimeServerMessage"],
+ [serverFixtures, "RealtimeServerMessage", "RealtimeClientMessage"],
+]))
{
for(const name of files)
{
const message = fixture(`realtime-${name}.message.json`);
assert(valid(accepted, message), `${name}: ${ajv.errorsText()}`);
assert(!valid(rejected, message), `${name}: wrong-direction frame accepted`);
- assert(Buffer.byteLength(JSON.stringify(message), 'utf8') <= settings.maxFrameBytes);
+ assert(Buffer.byteLength(JSON.stringify(message), "utf8") <= settings.maxFrameBytes);
const missing = { ...message };
- delete missing.payload;
+ delete missing["payload"];
assert(!valid(accepted, missing), `${name}: missing payload accepted`);
- assert(!valid(accepted, { ...message, contentUrl: 'https://unrelated.invalid/document' }));
+ assert(!valid(accepted, { ...message, contentUrl: "https://unrelated.invalid/document" }));
}
}
-const event = fixture('realtime-job-event.message.json');
-assert.deepEqual(fixture('realtime-job-event-replay.message.json'), event, 'Lost ACK replay preserves the exact envelope.');
-assert.equal(fixture('realtime-ack.message.json').payload.acknowledgedMessageId, event.messageId);
-assert.equal(fixture('realtime-error.message.json').messageId, event.messageId, 'Frame errors retain request correlation.');
-const largeJob = fixture('realtime-large-pdf.response.json');
-assert(valid('PrintJob', largeJob), ajv.errorsText());
+const event = fixture("realtime-job-event.message.json");
+assert.deepEqual(fixture("realtime-job-event-replay.message.json"), event, "Lost ACK replay preserves the exact envelope.");
+assert.equal((/** @type {typeof import("../fixtures/realtime-ack.message.json")} */ (/** @type {unknown} */ (fixture("realtime-ack.message.json")))).payload.acknowledgedMessageId, event["messageId"]);
+assert.equal(fixture("realtime-error.message.json")["messageId"], event["messageId"], "Frame errors retain request correlation.");
+const largeJob = /** @type {typeof import("../fixtures/realtime-large-pdf.response.json")} */ (/** @type {unknown} */ (fixture("realtime-large-pdf.response.json")));
+assert(valid("PrintJob", largeJob), ajv.errorsText());
assert.equal(largeJob.contentLength, settings.maxPdfBytes);
assert(largeJob.contentLength > 20 * 1024 * 1024);
-assert(valid('PrintOptions', { copies: 32767 }));
-assert(!valid('PrintOptions', { copies: 32768 }));
-console.log('Realtime directional frames, defaults/ranges, policy/ceiling refusal and exact replay fixtures passed; runtime concurrency acceptance remains consumer-owned.');
+assert(valid("PrintOptions", { copies: 32767 }));
+assert(!valid("PrintOptions", { copies: 32768 }));
+console.log("Realtime directional frames, defaults/ranges, policy/ceiling refusal and exact replay fixtures passed; runtime concurrency acceptance remains consumer-owned.");
diff --git a/tools/verify-schema.mjs b/tools/verify-schema.mjs
index 64b0e78..bb646cb 100644
--- a/tools/verify-schema.mjs
+++ b/tools/verify-schema.mjs
@@ -1,16 +1,17 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-import assert from 'node:assert/strict';
-import { readFileSync } from 'node:fs';
-import Ajv2020 from 'ajv/dist/2020.js';
-import addFormats from 'ajv-formats';
-import { fixtureTypes } from './fixture-types.mjs';
-import './verify-realtime.mjs';
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { Ajv2020 } from "ajv/dist/2020.js";
+import addFormats from "ajv-formats";
+import { fixtureTypes } from "./fixture-types.mjs";
+import "./verify-realtime.mjs";
-const schema = JSON.parse(readFileSync(new URL('../schemas/protocol.schema.json', import.meta.url), 'utf8'));
+const schema = JSON.parse(readFileSync(new URL("../schemas/protocol.schema.json", import.meta.url), "utf8"));
const ajv = new Ajv2020({ strict: true, allErrors: true });
-addFormats(ajv);
+// ajv-formats is callable CommonJS; its NodeNext declaration exposes a namespace.
+/** @type {import("ajv-formats").FormatsPlugin} */ (/** @type {unknown} */ (addFormats))(ajv);
assert(ajv.validateSchema(schema), ajv.errorsText());
ajv.addSchema(schema);
@@ -21,91 +22,98 @@ for(const name of Object.keys(schema.$defs))
}
for(const [file, type] of Object.entries(fixtureTypes))
{
- const value = JSON.parse(readFileSync(new URL(`../fixtures/${file}`, import.meta.url), 'utf8'));
+ const value = JSON.parse(readFileSync(new URL(`../fixtures/${file}`, import.meta.url), "utf8"));
const validate = ajv.compile({ $ref: `${schema.$id}#/$defs/${type}` });
assert(validate(value), `${file}: ${ajv.errorsText(validate.errors)}`);
}
const validateTest = ajv.compile({ $ref: `${schema.$id}#/$defs/DeviceTestPrintRequest` });
-const testRequest = { printerId: 'printer-zebra', format: 'ZPL', requestId: 'test-1' };
-assert(validateTest(testRequest), 'Existing requests without a profile remain valid.');
-assert(validateTest({ ...testRequest, testProfile: 'LABEL_4X6_300' }));
-assert(!validateTest({ ...testRequest, testProfile: 'LETTER' }));
+const testRequest = { printerId: "printer-zebra", format: "ZPL", requestId: "test-1" };
+assert(validateTest(testRequest), "Existing requests without a profile remain valid.");
+assert(validateTest({ ...testRequest, testProfile: "LABEL_4X6_300" }));
+assert(validateTest({ ...testRequest, testProfile: "LABEL_4X1_203" }));
+assert(validateTest({ ...testRequest, testProfile: "LABEL_4X2_600" }));
+assert(validateTest({ ...testRequest, format: "PDF", testProfile: "LABEL_4X2_300" }));
+assert(!validateTest({ ...testRequest, testProfile: "LABEL_4X3_300" }));
+assert(!validateTest({ ...testRequest, testProfile: "LETTER" }));
assert(!validateTest({ ...testRequest, testProfile: null }));
-assert(!validateTest({ format: 'PDF' }));
+assert(!validateTest({ format: "PDF" }));
const validatePrinters = ajv.compile({ $ref: `${schema.$id}#/$defs/PrinterList` });
-const printerPage = JSON.parse(readFileSync(new URL('../fixtures/printers-page.response.json', import.meta.url), 'utf8'));
+/** @type {typeof import("../fixtures/printers-page.response.json")} */
+const printerPage = JSON.parse(readFileSync(new URL("../fixtures/printers-page.response.json", import.meta.url), "utf8"));
assert(validatePrinters(printerPage));
-assert(validatePrinters({ printers: printerPage.printers }), 'Older responses without a cursor remain valid.');
-assert(validatePrinters({ ...printerPage, nextCursor: 'opaque-server-cursor' }));
+assert(validatePrinters({ printers: printerPage.printers }), "Older responses without a cursor remain valid.");
+assert(validatePrinters({ ...printerPage, nextCursor: "opaque-server-cursor" }));
assert(!validatePrinters({ ...printerPage, nextCursor: 17 }));
-assert(!validatePrinters({ ...printerPage, nextCursor: 'x'.repeat(12001) }));
+assert(!validatePrinters({ ...printerPage, nextCursor: "x".repeat(12001) }));
assert(!validatePrinters({ printers: Array(201).fill(printerPage.printers[0]) }));
-const control = name => JSON.parse(readFileSync(new URL(`../fixtures/${name}`, import.meta.url), 'utf8'));
+/** @param {string} name @returns {Record} */
+const control = name => JSON.parse(readFileSync(new URL(`../fixtures/${name}`, import.meta.url), "utf8"));
+/** @param {string} type @param {unknown} value @returns {boolean|Promise} */
const validControl = (type, value) => ajv.validate(`${schema.$id}#/$defs/${type}`, value);
-assert(!validControl('PrintingSessionIdentity', { tenantId: 'test-tenant' }), 'Display or device identity cannot replace the authoritative principal.');
-assert(!validControl('PrintingSessionIdentity', { principalId: 'test-operator', tenantId: '' }));
-assert(!validControl('PrintingSessionIdentity', { principalId: 'test-operator', tenantId: 'test-tenant', deviceId: null }));
-assert(validControl('PrintingSessionIdentity', { principalId: 'test-operator', tenantId: 'test-tenant', futureResponseField: true }));
-const action = control('action-clear-queue.request.json');
-assert(validControl('AdminDeviceActionRequest', action));
+assert(!validControl("PrintingSessionIdentity", { tenantId: "test-tenant" }), "Display or device identity cannot replace the authoritative principal.");
+assert(!validControl("PrintingSessionIdentity", { principalId: "test-operator", tenantId: "" }));
+assert(!validControl("PrintingSessionIdentity", { principalId: "test-operator", tenantId: "test-tenant", deviceId: null }));
+assert(validControl("PrintingSessionIdentity", { principalId: "test-operator", tenantId: "test-tenant", futureResponseField: true }));
+const action = control("action-clear-queue.request.json");
+assert(validControl("AdminDeviceActionRequest", action));
const withoutIssuedAt = { ...action };
-delete withoutIssuedAt.issuedAt;
-assert(!validControl('AdminDeviceActionRequest', withoutIssuedAt), 'New controls require their durable freshness timestamp.');
+delete withoutIssuedAt["issuedAt"];
+assert(!validControl("AdminDeviceActionRequest", withoutIssuedAt), "New controls require their durable freshness timestamp.");
for(const invalid of [
- { issuedAt: '2026-10-31T16:00:00-05:00' },
+ { issuedAt: "2026-10-31T16:00:00-05:00" },
{ issuedAt: null },
- { action: 'RESTART_SERVICE' },
- { action: 'CANCEL_SPOOL' },
- { jobId: 'job-example-1' },
- { spoolerJobId: '17' },
- { queueName: 'Some other queue' },
- { command: 'shell' },
- { requestId: '../device' }
+ { action: "RESTART_SERVICE" },
+ { action: "CANCEL_SPOOL" },
+ { jobId: "job-example-1" },
+ { spoolerJobId: "17" },
+ { queueName: "Some other queue" },
+ { command: "shell" },
+ { requestId: "../device" },
])
{
- assert(!validControl('AdminDeviceActionRequest', { ...action, ...invalid }), JSON.stringify(invalid));
+ assert(!validControl("AdminDeviceActionRequest", { ...action, ...invalid }), JSON.stringify(invalid));
}
-const poll = control('control-poll.request.json');
-assert(!validControl('DeviceControlPollRequest', { ...poll, controlVersion: 2 }));
-assert(!validControl('DeviceControlPollRequest', { ...poll, capabilities: ['CANCEL_SPOOL', 'CANCEL_SPOOL'] }));
-assert(!validControl('DeviceControlPollRequest', { ...poll, capabilities: ['RESTART_SERVICE'] }));
-assert(!validControl('DeviceControlPollRequest', { ...poll, lastAppliedRevision: -1 }));
-assert(!validControl('DeviceControlPollRequest', { ...poll, lastAppliedRevision: 9007199254740992 }));
-const pollResult = control('control-poll.response.json');
-assert(validControl('DeviceControlPollResponse', { ...pollResult, futureResponseField: true }), 'Additive response fields remain compatible.');
-assert(!validControl('DeviceControlPollResponse', { ...pollResult, commands: Array(21).fill(pollResult.commands[0]) }));
-assert(!validControl('DeviceControlPollResponse', { ...pollResult, desiredState: { ...pollResult.desiredState, connectionState: 'REVOKED' } }), 'Revocation is credential invalidation, not reversible desired state.');
-assert(!validControl('DeviceCommand', { ...pollResult.commands[1], jobId: null }));
-assert(!validControl('DeviceCommand', { ...pollResult.commands[0], jobId: 'job-example-1' }));
-assert(!validControl('DeviceCommand', { ...pollResult.commands[0], action: 'CLEAR_QUEUE' }), 'Server queue clear is never an arbitrary client command.');
-const receipt = control('control-cancel-requested.receipt.json');
-assert(!validControl('DeviceCommandReceipt', { ...receipt, outcome: 'PRINT_RECALLED' }));
-assert(!validControl('DeviceCommandReceipt', { ...receipt, message: 'x'.repeat(1025) }));
-assert(!validControl('DeviceCommandReceipt', { ...receipt, completedAt: 'not-a-date' }));
-assert(!validControl('DeviceCommandReceipt', { ...receipt, jobId: 'other-job' }), 'Receipt cannot override its route-bound command target.');
-const history = control('control-commands-page.response.json');
-assert(!validControl('DeviceActionList', { ...history, commands: Array(201).fill(history.commands[0]) }));
-assert(!validControl('DeviceActionList', { ...history, nextCursor: 'x'.repeat(12001) }));
+const poll = control("control-poll.request.json");
+assert(!validControl("DeviceControlPollRequest", { ...poll, controlVersion: 2 }));
+assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["CANCEL_SPOOL", "CANCEL_SPOOL"] }));
+assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["RESTART_SERVICE"] }));
+assert(!validControl("DeviceControlPollRequest", { ...poll, lastAppliedRevision: -1 }));
+assert(!validControl("DeviceControlPollRequest", { ...poll, lastAppliedRevision: 9007199254740992 }));
+const pollResult = /** @type {typeof import("../fixtures/control-poll.response.json")} */ (/** @type {unknown} */ (control("control-poll.response.json")));
+assert(validControl("DeviceControlPollResponse", { ...pollResult, futureResponseField: true }), "Additive response fields remain compatible.");
+assert(!validControl("DeviceControlPollResponse", { ...pollResult, commands: Array(21).fill(pollResult.commands[0]) }));
+assert(!validControl("DeviceControlPollResponse", { ...pollResult, desiredState: { ...pollResult.desiredState, connectionState: "REVOKED" } }), "Revocation is credential invalidation, not reversible desired state.");
+assert(!validControl("DeviceCommand", { ...pollResult.commands[1], jobId: null }));
+assert(!validControl("DeviceCommand", { ...pollResult.commands[0], jobId: "job-example-1" }));
+assert(!validControl("DeviceCommand", { ...pollResult.commands[0], action: "CLEAR_QUEUE" }), "Server queue clear is never an arbitrary client command.");
+const receipt = control("control-cancel-requested.receipt.json");
+assert(!validControl("DeviceCommandReceipt", { ...receipt, outcome: "PRINT_RECALLED" }));
+assert(!validControl("DeviceCommandReceipt", { ...receipt, message: "x".repeat(1025) }));
+assert(!validControl("DeviceCommandReceipt", { ...receipt, completedAt: "not-a-date" }));
+assert(!validControl("DeviceCommandReceipt", { ...receipt, jobId: "other-job" }), "Receipt cannot override its route-bound command target.");
+const history = /** @type {typeof import("../fixtures/control-commands-page.response.json")} */ (/** @type {unknown} */ (control("control-commands-page.response.json")));
+assert(!validControl("DeviceActionList", { ...history, commands: Array(201).fill(history.commands[0]) }));
+assert(!validControl("DeviceActionList", { ...history, nextCursor: "x".repeat(12001) }));
console.log(`Compiled ${Object.keys(schema.$defs).length} JSON Schema 2020-12 definitions; ${Object.keys(fixtureTypes).length} fixtures passed full schema validation.`);
-const media = control('printer-media-profile.response.json');
-for(const invalid of [{ dpiX: 0 }, { widthMicrons: 0 }, { revision: 'not-a-revision' }, { applyMode: 'CHANGE_STOCK' }])
+const media = control("printer-media-profile.response.json");
+for(const invalid of [{ dpiX: 0 }, { widthMicrons: 0 }, { revision: "not-a-revision" }, { applyMode: "CHANGE_STOCK" }])
{
- assert(!validControl('PrinterMediaProfile', { ...media, ...invalid }));
+ assert(!validControl("PrinterMediaProfile", { ...media, ...invalid }));
}
-const selection = control('media-profile-options.request.json');
-assert(!validControl('PrintOptions', { ...selection, paperWidthMm: 100, paperHeightMm: 150 }));
-assert(!validControl('PrinterMediaSelection', { ...selection.mediaProfile, futureOption: true }));
+const selection = /** @type {typeof import("../fixtures/media-profile-options.request.json")} */ (/** @type {unknown} */ (control("media-profile-options.request.json")));
+assert(!validControl("PrintOptions", { ...selection, paperWidthMm: 100, paperHeightMm: 150 }));
+assert(!validControl("PrinterMediaSelection", { ...selection.mediaProfile, futureOption: true }));
-const operatorRequest = control('operator-submit.request.json');
-for(const invalid of [{ requestId: 'x'.repeat(129) }, { requestId: 'echo/scan' }, { actorId: 'forged' }, { tenantId: 'forged' }])
+const operatorRequest = control("operator-submit.request.json");
+for(const invalid of [{ requestId: "x".repeat(129) }, { requestId: "echo/scan" }, { actorId: "forged" }, { tenantId: "forged" }])
{
- assert(!validControl('OperatorSubmitJobRequest', { ...operatorRequest, ...invalid }));
+ assert(!validControl("OperatorSubmitJobRequest", { ...operatorRequest, ...invalid }));
}
-assert(validControl('OperatorSubmitJobRequest', { ...operatorRequest, requestId: 'x'.repeat(128) }));
+assert(validControl("OperatorSubmitJobRequest", { ...operatorRequest, requestId: "x".repeat(128) }));
-const catalogEntry = control('operator-printers.response.json').printers[0];
-assert(validControl('OperatorPrinter', { ...catalogEntry, deviceName: 'x'.repeat(1024) }));
-assert(!validControl('OperatorPrinter', { ...catalogEntry, deviceName: 'x'.repeat(1025) }));
-assert(validControl('PrinterMediaProfile', { ...media, id: 'x'.repeat(128), futureMetadata: { supported: true } }));
-assert(!validControl('PrinterMediaProfile', { ...media, id: 'x'.repeat(129) }));
+const catalogEntry = (/** @type {typeof import("../fixtures/operator-printers.response.json")} */ (/** @type {unknown} */ (control("operator-printers.response.json")))).printers[0];
+assert(validControl("OperatorPrinter", { ...catalogEntry, deviceName: "x".repeat(1024) }));
+assert(!validControl("OperatorPrinter", { ...catalogEntry, deviceName: "x".repeat(1025) }));
+assert(validControl("PrinterMediaProfile", { ...media, id: "x".repeat(128), futureMetadata: { supported: true } }));
+assert(!validControl("PrinterMediaProfile", { ...media, id: "x".repeat(129) }));
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..3a84786
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "target": "ES2024",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": [
+ "ES2024"
+ ],
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "useUnknownInCatchVariables": true,
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "sourceMap": true,
+ "newLine": "lf",
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "types": [
+ "node"
+ ]
+ },
+ "include": [
+ "tools/**/*.mjs",
+ "eslint.config.mjs"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "build",
+ "coverage"
+ ]
+}
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..0e343f6
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,43 @@
+version = 1
+revision = 5
+requires-python = ">=3.13"
+
+[[package]]
+name = "qqq-print-protocol-script-quality"
+version = "0.0.0"
+source = { virtual = "." }
+
+[package.dev-dependencies]
+dev = [
+ { name = "ruff" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [{ name = "ruff", specifier = "==0.17.3" }]
+
+[[package]]
+name = "ruff"
+version = "0.17.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" },
+ { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" },
+ { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" },
+ { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" },
+ { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" },
+ { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" },
+ { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" },
+]