Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions python/tests/test_qsl_compat_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,103 @@ def test_not_enforced_bundle_reports_warning_for_short_sha_and_mismatch_but_main
self.assertTrue(any("bundle pin mismatch for QuantPlatformKit" in warning for warning in warnings))
self.assertEqual(notes[0], "qsl=" + str(repo_root / "qsl.toml"))

def test_live_constraint_files_allow_full_sha_drift_from_bundle(self):
with tempfile.TemporaryDirectory() as workspace:
compat_root = Path(workspace)
self._write_bundle(
compat_root,
"2026.07.1",
{"QuantPlatformKit": "a" * 40},
)
repo_root = self._make_repo_root(
qsl_toml=(
'tier = "core"\n'
"ring = 0\n"
"allow_legacy = true\n"
"[compat]\n"
'bundle = "2026.07.1"\n'
'live_constraint_files = ["constraints.txt"]\n'
),
pyproject="",
)
(repo_root / "constraints.txt").write_text(
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/"
f"QuantPlatformKit.git@{'b' * 40}\n",
encoding="utf-8",
)

ok, issues, warnings, notes = check_qsl_compat._check(repo_root=repo_root, compat_root=compat_root)

self.assertTrue(ok)
self.assertEqual(issues, [])
self.assertEqual(warnings, [])
self.assertIn("live_constraint_files=constraints.txt", notes)

def test_live_constraint_files_still_block_short_refs(self):
with tempfile.TemporaryDirectory() as workspace:
compat_root = Path(workspace)
self._write_bundle(
compat_root,
"2026.07.1",
{"QuantPlatformKit": "a" * 40},
)
repo_root = self._make_repo_root(
qsl_toml=(
'tier = "core"\n'
"ring = 0\n"
"allow_legacy = true\n"
"[compat]\n"
'bundle = "2026.07.1"\n'
'live_constraint_files = ["constraints.txt"]\n'
),
pyproject="",
)
(repo_root / "constraints.txt").write_text(
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@abc123\n",
encoding="utf-8",
)

ok, issues, warnings, notes = check_qsl_compat._check(repo_root=repo_root, compat_root=compat_root)

self.assertFalse(ok)
self.assertEqual(len(issues), 1)
self.assertIn("forbidden short/invalid ref 'abc123'", issues[0])
self.assertEqual(warnings, [])

def test_legacy_reason_suppresses_allowed_legacy_warning(self):
with tempfile.TemporaryDirectory() as workspace:
compat_root = Path(workspace)
self._write_bundle(
compat_root,
"2026.07.1",
{"QuantPlatformKit": "a" * 40},
)
repo_root = self._make_repo_root(
qsl_toml=(
'tier = "pipeline"\n'
"ring = 2\n"
"allow_legacy = true\n"
'legacy_reason = "runtime deployment compatibility"\n'
"[compat]\n"
'bundle = "2026.07.1"\n'
"enforce_bundle = false\n"
),
pyproject="",
)
(repo_root / "requirements.txt").write_text(
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/"
f"QuantPlatformKit.git@{'b' * 40}\n",
encoding="utf-8",
)

ok, issues, warnings, notes = check_qsl_compat._check(repo_root=repo_root, compat_root=compat_root)

self.assertTrue(ok)
self.assertEqual(issues, [])
self.assertEqual(len(warnings), 1)
self.assertIn("bundle pin mismatch", warnings[0])
self.assertIn("legacy_reason=runtime deployment compatibility", notes)


if __name__ == "__main__":
unittest.main()
42 changes: 39 additions & 3 deletions scripts/check_qsl_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ def _get_enforce_bundle(config: dict[str, Any]) -> bool:
return bool(config.get("enforce_bundle", True))


def _load_qsl_config(repo_root: Path) -> dict[str, str | bool]:
def _string_list(value: Any) -> list[str]:
if not isinstance(value, list):
return []
return [str(item).strip() for item in value if str(item).strip()]


def _load_qsl_config(repo_root: Path) -> dict[str, str | bool | list[str]]:
qsl_path = repo_root / "qsl.toml"
if not qsl_path.exists():
raise FileNotFoundError(f"missing qsl.toml: {qsl_path}")
Expand All @@ -84,14 +90,21 @@ def _load_qsl_config(repo_root: Path) -> dict[str, str | bool]:
upgrade_ring = _get_upgrade_ring(config)

allow_legacy = bool(config.get("allow_legacy", False))
legacy_reason = str(config.get("legacy_reason", "")).strip()
enforce_bundle = _get_enforce_bundle(config)
live_constraint_files = _string_list(config.get("live_constraint_files"))
compat = config.get("compat")
if isinstance(compat, dict):
live_constraint_files.extend(_string_list(compat.get("live_constraint_files")))

return {
"bundle": bundle,
"tier": tier.strip(),
"upgrade_ring": upgrade_ring,
"allow_legacy": allow_legacy,
"legacy_reason": legacy_reason,
"enforce_bundle": enforce_bundle,
"live_constraint_files": sorted(set(live_constraint_files)),
"qsl_path": qsl_path.as_posix(),
}

Expand Down Expand Up @@ -170,13 +183,19 @@ def _check(repo_root: Path, compat_root: Path) -> tuple[bool, list[str], list[st
upgrade_ring = str(qsl_cfg["upgrade_ring"])
allow_legacy = bool(qsl_cfg["allow_legacy"])
enforce_bundle = bool(qsl_cfg["enforce_bundle"])
legacy_reason = str(qsl_cfg["legacy_reason"])
live_constraint_files = set(qsl_cfg["live_constraint_files"]) if isinstance(qsl_cfg["live_constraint_files"], list) else set()
qsl_path = str(qsl_cfg["qsl_path"])

notes.append(f"qsl={qsl_path}")
notes.append(f"bundle={bundle}")
notes.append(f"tier={tier}")
notes.append(f"upgrade_ring={upgrade_ring}")
notes.append(f"enforce_bundle={enforce_bundle}")
if legacy_reason:
notes.append("legacy_reason=" + legacy_reason)
if live_constraint_files:
notes.append("live_constraint_files=" + ",".join(sorted(live_constraint_files)))

bundle_refs = _load_bundle(compat_root, bundle)

Expand All @@ -187,15 +206,20 @@ def _check(repo_root: Path, compat_root: Path) -> tuple[bool, list[str], list[st
else:
legacy_refs = _gather_legacy_refs(repo_root)
if legacy_refs:
warnings.append("legacy dependency files detected but allowed by qsl.allow_legacy=true")
static_legacy_refs = [ref for ref in legacy_refs if ref.source not in live_constraint_files]
if static_legacy_refs and not legacy_reason:
warnings.append("legacy dependency files detected but allowed by qsl.allow_legacy=true")
for legacy_ref in legacy_refs:
if legacy_ref.repo not in bundle_refs:
issues.append(
f"unmanaged qsl dependency in {legacy_ref.source}:{legacy_ref.line_no}: "
f"{legacy_ref.repo}@{legacy_ref.ref}"
)
continue
_validate_ref(legacy_ref, bundle_refs[legacy_ref.repo], issues, warnings, enforce_bundle)
if legacy_ref.source in live_constraint_files:
_validate_live_ref(legacy_ref, issues, warnings, enforce_bundle)
Comment on lines +219 to +220

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor configured live constraint paths

When a repo declares live_constraint_files with a generated file name other than exactly requirements.txt or constraints.txt (for example live-constraints.txt), those refs never reach this branch because _gather_legacy_refs still only extracts the two hard-coded legacy files. The checker still notes live_constraint_files=... and returns ok even if the declared live file contains @main or short refs, so live deployments can bypass the compatibility gate; include the configured files in the extraction set and match paths consistently before applying live validation.

Useful? React with 👍 / 👎.

else:
_validate_ref(legacy_ref, bundle_refs[legacy_ref.repo], issues, warnings, enforce_bundle)

discovered = _gather_repo_refs(repo_root)
for pin in discovered:
Expand Down Expand Up @@ -230,6 +254,18 @@ def _validate_ref(pin: GitRef, expected_ref: str, issues: list[str], warnings: l
warnings.append(message)


def _validate_live_ref(pin: GitRef, issues: list[str], warnings: list[str], enforce_bundle: bool) -> None:
if _is_main_ref(pin.ref):
issues.append(f"forbidden ref 'main' in {pin.source}:{pin.line_no}: {pin.repo}")
return
if not _is_full_sha(pin.ref):
message = f"forbidden short/invalid ref '{pin.ref}' in {pin.source}:{pin.line_no}: {pin.repo}"
if enforce_bundle:
issues.append(message)
else:
warnings.append(message)
Comment on lines +263 to +266

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep live constraint refs fatal in transition mode

When a repo declares live_constraint_files but also sets enforce_bundle = false for a transition, a short or otherwise invalid ref in the live constraint file is downgraded to a warning here, so _check returns ok=True because there are no issues. That lets generated live deployment pins use abc123 (or similar non-full-SHA refs) while the compatibility gate still passes; live constraints should only relax bundle-drift checks, not allow non-immutable refs.

Useful? React with 👍 / 👎.



def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Check repo QSL compatibility against the central manifest.")
parser.add_argument("--repo-root", default=".", type=Path, help="Repository root to check")
Expand Down
Loading