Skip to content

Commit fc4b087

Browse files
Pigbibicodex
andcommitted
feat: track QSL bundle exception metadata
Co-Authored-By: Codex <noreply@openai.com>
1 parent 3d1b4fa commit fc4b087

6 files changed

Lines changed: 130 additions & 44 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ This repository is the central manifest source for QSL compatibility checks.
3535
- `scripts/check_qsl_compat.py`: validate a repo's QSL compliance in `pyproject.toml`/`uv.lock` against central bundle.
3636
- `python/scripts/qslctl.py`: unified QSL version-control CLI for repo checks, workspace checks, ring reports/plans, and matrix generation.
3737
- `scripts/render_qsl_dependency_graph.py`: render dependency graph for review.
38-
- `compat/bundles/2026.07.0.toml`: first compatibility bundle (CalVer).
38+
- `compat/bundles/2026.07.2.toml`: current compatibility bundle baseline (CalVer).
3939
- `compat/repo-tiers.toml`: repo tier and upgrade ring policy notes.
4040
- [QSL compatibility docs](docs/qsl_compat_upgrade.md)
4141

docs/qsl_compat_upgrade.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This repository defines the QSL central compatibility manifest and its upgrade p
55
## 文件结构
66

77
- `compat/bundles/*.toml`
8-
- 每个 bundle 用 Calendar Version 命名(当前:`2026.07.1`)。
8+
- 每个 bundle 用 Calendar Version 命名(当前:`2026.07.2`)。
99
- 记录 QSL 管控的固定内部仓库提交。
1010
- `compat/repo-tiers.toml`
1111
- 记录仓库层级(`core/strategy-lib/pipeline/runtime/ops`)与升级 ring 规则。
@@ -26,8 +26,8 @@ This repository defines the QSL central compatibility manifest and its upgrade p
2626

2727
```toml
2828
[qsl]
29-
bundle = "2026.07.1" # 选择要对齐的 central bundle
30-
compat = "2026.07.1" # 兼容检查入口(与 bundle 相同)
29+
bundle = "2026.07.2" # 选择要对齐的 central bundle
30+
compat = "2026.07.2" # 兼容检查入口(与 bundle 相同)
3131
tier = "ops/tooling"
3232
upgrade_ring = "ring_e"
3333
allow_legacy = false # 需要临时兼容时可先放开
@@ -48,16 +48,17 @@ python scripts/render_qsl_dependency_graph.py --repo-root . --format md
4848
- 阶段过渡仓可设置 `enforce_bundle = false`(建议限时):
4949
- `forbidden short/invalid``bundle pin mismatch` 降级为 warning。
5050
- `forbidden ref 'main'` 始终为 issue,不降级。
51+
- 当前 checker 已识别 `legacy_reason``live_constraint_files``owner` / `expires_at` / `next_action` 已进入 checker warning,用于约束过渡例外的负责人、到期日和下一步动作。
5152
- `--non-strict` 仅用于本地快速预览,不作为发布门禁依据。
5253

5354
## 当前中心兼容基线
5455

55-
- Bundle: `2026.07.1`
56-
- QPK: `7032cde4547e7ec59af15df8935d142461a77051`
57-
- UsEquityStrategies: `9f0e5e2deca8a9c16d711eb4772f08a7901da101`
58-
- HkEquityStrategies: `dbbefb688cd144837aa59581b1930a14c11411ad`
59-
- CnEquityStrategies: `357dba7e8896a7f488a484d4a3eea33894708ab9`
60-
- CryptoStrategies: `64a62781f9194a23548a373c7724e132ef311f1f`
56+
- Bundle: `2026.07.2`
57+
- QPK: `37c81901160c5b31127a27dba1c63944933fb6bf`
58+
- UsEquityStrategies: `17ddb86c72d44b2c7b78ba7a10d8f71b21180166`
59+
- HkEquityStrategies: `b6a8ac2ad3c8110b5ea74fb059c8206388d63bcd`
60+
- CnEquityStrategies: `8dfadcf8a4dc6cc516f27a4013248474603d8ce2`
61+
- CryptoStrategies: `39bf4733cef922bdeacfd0adef394e7819a04908`
6162
- QuantStrategyPlugins: `1f3a27b8fd83d71b583f4f5160a748e95fbefaa1`
6263
- MarketSignalSources: `bda8ab10b80a646e4b579ef4c0d5dfe90776e908`
6364

python/tests/test_qsl_compat_checker.py

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,19 @@ def test_root_compat_bundle_and_ring_schema(self):
3535
compat_root = Path(workspace)
3636
self._write_bundle(
3737
compat_root,
38-
"2026.07.1",
39-
{"QuantPlatformKit": "7032cde4547e7ec59af15df8935d142461a77051"},
38+
"2026.07.2",
39+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
4040
)
4141
repo_root = self._make_repo_root(
4242
qsl_toml=(
4343
"tier = \"ops/tooling\"\n"
4444
"ring = \"ring_e\"\n"
4545
"[compat]\n"
46-
'bundle = "2026.07.1"\n'
46+
'bundle = "2026.07.2"\n'
4747
),
4848
pyproject=(
4949
"dependencies = [\n"
50-
' "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@7032cde4547e7ec59af15df8935d142461a77051"\n'
50+
' "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@37c81901160c5b31127a27dba1c63944933fb6bf"\n'
5151
"]\n"
5252
),
5353
)
@@ -57,26 +57,26 @@ def test_root_compat_bundle_and_ring_schema(self):
5757
self.assertTrue(ok)
5858
self.assertEqual(issues, [])
5959
self.assertEqual(warnings, [])
60-
self.assertIn("bundle=2026.07.1", notes)
60+
self.assertIn("bundle=2026.07.2", notes)
6161
self.assertIn("upgrade_ring=ring_e", notes)
6262

6363
def test_not_enforced_bundle_reports_warning_for_short_sha_and_mismatch_but_main_stays_issue(self):
6464
with tempfile.TemporaryDirectory() as workspace:
6565
compat_root = Path(workspace)
6666
self._write_bundle(
6767
compat_root,
68-
"2026.07.1",
68+
"2026.07.2",
6969
{
70-
"QuantPlatformKit": "7032cde4547e7ec59af15df8935d142461a77051",
71-
"UsEquityStrategies": "9f0e5e2deca8a9c16d711eb4772f08a7901da101",
70+
"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf",
71+
"UsEquityStrategies": "17ddb86c72d44b2c7b78ba7a10d8f71b21180166",
7272
},
7373
)
7474
repo_root = self._make_repo_root(
7575
qsl_toml=(
7676
"tier = \"ops/tooling\"\n"
7777
"ring = \"ring_e\"\n"
7878
"[compat]\n"
79-
'bundle = "2026.07.1"\n'
79+
'bundle = "2026.07.2"\n'
8080
"enforce_bundle = false\n"
8181
),
8282
pyproject=(
@@ -94,26 +94,27 @@ def test_not_enforced_bundle_reports_warning_for_short_sha_and_mismatch_but_main
9494
self.assertFalse(ok)
9595
self.assertEqual(len(issues), 1)
9696
self.assertIn("forbidden ref 'main'", issues[0])
97-
self.assertEqual(len(warnings), 2)
97+
self.assertEqual(len(warnings), 3)
9898
self.assertTrue(any("forbidden short/invalid ref 'abc123'" in warning for warning in warnings))
9999
self.assertTrue(any("bundle pin mismatch for QuantPlatformKit" in warning for warning in warnings))
100+
self.assertTrue(any("missing exception metadata" in warning for warning in warnings))
100101
self.assertEqual(notes[0], "qsl=" + str(repo_root / "qsl.toml"))
101102

102103
def test_live_constraint_files_allow_full_sha_drift_from_bundle(self):
103104
with tempfile.TemporaryDirectory() as workspace:
104105
compat_root = Path(workspace)
105106
self._write_bundle(
106107
compat_root,
107-
"2026.07.1",
108-
{"QuantPlatformKit": "a" * 40},
108+
"2026.07.2",
109+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
109110
)
110111
repo_root = self._make_repo_root(
111112
qsl_toml=(
112113
'tier = "core"\n'
113114
"ring = 0\n"
114115
"allow_legacy = true\n"
115116
"[compat]\n"
116-
'bundle = "2026.07.1"\n'
117+
'bundle = "2026.07.2"\n'
117118
'live_constraint_files = ["constraints.txt"]\n'
118119
),
119120
pyproject="",
@@ -136,16 +137,16 @@ def test_live_constraint_files_still_block_short_refs(self):
136137
compat_root = Path(workspace)
137138
self._write_bundle(
138139
compat_root,
139-
"2026.07.1",
140-
{"QuantPlatformKit": "a" * 40},
140+
"2026.07.2",
141+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
141142
)
142143
repo_root = self._make_repo_root(
143144
qsl_toml=(
144145
'tier = "core"\n'
145146
"ring = 0\n"
146147
"allow_legacy = true\n"
147148
"[compat]\n"
148-
'bundle = "2026.07.1"\n'
149+
'bundle = "2026.07.2"\n'
149150
'live_constraint_files = ["constraints.txt"]\n'
150151
),
151152
pyproject="",
@@ -162,22 +163,57 @@ def test_live_constraint_files_still_block_short_refs(self):
162163
self.assertIn("forbidden short/invalid ref 'abc123'", issues[0])
163164
self.assertEqual(warnings, [])
164165

166+
167+
def test_not_enforced_bundle_exception_metadata_notes(self):
168+
with tempfile.TemporaryDirectory() as workspace:
169+
compat_root = Path(workspace)
170+
self._write_bundle(
171+
compat_root,
172+
"2026.07.2",
173+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
174+
)
175+
repo_root = self._make_repo_root(
176+
qsl_toml=(
177+
'tier = "pipeline"\n'
178+
"ring = 2\n"
179+
'owner = "pipeline-team"\n'
180+
'expires_at = "2099-12-31"\n'
181+
'next_action = "remove transition pin drift"\n'
182+
"[compat]\n"
183+
'bundle = "2026.07.2"\n'
184+
"enforce_bundle = false\n"
185+
),
186+
pyproject="",
187+
)
188+
189+
ok, issues, warnings, notes = check_qsl_compat._check(repo_root=repo_root, compat_root=compat_root)
190+
191+
self.assertTrue(ok)
192+
self.assertEqual(issues, [])
193+
self.assertEqual(warnings, [])
194+
self.assertIn("owner=pipeline-team", notes)
195+
self.assertIn("expires_at=2099-12-31", notes)
196+
self.assertIn("next_action=remove transition pin drift", notes)
197+
165198
def test_legacy_reason_suppresses_allowed_legacy_warning(self):
166199
with tempfile.TemporaryDirectory() as workspace:
167200
compat_root = Path(workspace)
168201
self._write_bundle(
169202
compat_root,
170-
"2026.07.1",
171-
{"QuantPlatformKit": "a" * 40},
203+
"2026.07.2",
204+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
172205
)
173206
repo_root = self._make_repo_root(
174207
qsl_toml=(
175208
'tier = "pipeline"\n'
176209
"ring = 2\n"
177210
"allow_legacy = true\n"
178211
'legacy_reason = "runtime deployment compatibility"\n'
212+
'owner = "runtime-team"\n'
213+
'expires_at = "2099-12-31"\n'
214+
'next_action = "replace runtime deployment requirements"\n'
179215
"[compat]\n"
180-
'bundle = "2026.07.1"\n'
216+
'bundle = "2026.07.2"\n'
181217
"enforce_bundle = false\n"
182218
),
183219
pyproject="",

python/tests/test_qslctl.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ def test_check_all_reports_repo_issues(self) -> None:
2424
with tempfile.TemporaryDirectory() as workspace:
2525
root = Path(workspace)
2626
compat_root = root / "QuantRuntimeSettings"
27-
self._write_bundle(compat_root, "2026.07.1", {"QuantPlatformKit": "a" * 40})
27+
self._write_bundle(compat_root, "2026.07.2", {"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"})
2828
good = root / "GoodRepo"
2929
bad = root / "BadRepo"
30-
self._write_repo(good, "2026.07.1", "a" * 40)
31-
self._write_repo(bad, "2026.07.1", "b" * 40)
30+
self._write_repo(good, "2026.07.2", "37c81901160c5b31127a27dba1c63944933fb6bf")
31+
self._write_repo(bad, "2026.07.2", "b" * 40)
3232

3333
with patch.object(qslctl, "_is_quant_repo", return_value=True):
3434
results = qslctl.check_all(projects_root=root, compat_root=compat_root)
@@ -44,17 +44,17 @@ def test_report_groups_repositories_by_ring(self) -> None:
4444
compat_root = root / "QuantRuntimeSettings"
4545
self._write_bundle(
4646
compat_root,
47-
"2026.07.1",
48-
{"QuantPlatformKit": "a" * 40},
47+
"2026.07.2",
48+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
4949
)
5050
self._write_repo_tiers(compat_root)
5151

5252
core = root / "CoreRepo"
5353
warning = root / "WarningRepo"
5454
clean = root / "CleanRepo"
55-
self._write_repo(core, "2026.07.1", "b" * 40, tier="core", ring="ring_a")
56-
self._write_repo(warning, "2026.07.1", "b" * 40, tier="strategy-lib", ring="ring_b", enforce_bundle=False)
57-
self._write_repo(clean, "2026.07.1", "a" * 40, tier="pipeline", ring="ring_c")
55+
self._write_repo(core, "2026.07.2", "b" * 40, tier="core", ring="ring_a")
56+
self._write_repo(warning, "2026.07.2", "b" * 40, tier="strategy-lib", ring="ring_b", enforce_bundle=False)
57+
self._write_repo(clean, "2026.07.2", "37c81901160c5b31127a27dba1c63944933fb6bf", tier="pipeline", ring="ring_c")
5858

5959
buf = io.StringIO()
6060
with patch.object(qslctl, "_is_quant_repo", return_value=True), contextlib.redirect_stdout(buf):
@@ -83,14 +83,14 @@ def test_plan_orders_rings_and_actions(self) -> None:
8383
compat_root = root / "QuantRuntimeSettings"
8484
self._write_bundle(
8585
compat_root,
86-
"2026.07.1",
87-
{"QuantPlatformKit": "a" * 40},
86+
"2026.07.2",
87+
{"QuantPlatformKit": "37c81901160c5b31127a27dba1c63944933fb6bf"},
8888
)
8989
self._write_repo_tiers(compat_root)
9090

91-
self._write_repo(root / "CoreRepo", "2026.07.1", "b" * 40, tier="core", ring="ring_a")
92-
self._write_repo(root / "WarningRepo", "2026.07.1", "b" * 40, tier="strategy-lib", ring="ring_b", enforce_bundle=False)
93-
self._write_repo(root / "CleanRepo", "2026.07.1", "a" * 40, tier="pipeline", ring="ring_c")
91+
self._write_repo(root / "CoreRepo", "2026.07.2", "b" * 40, tier="core", ring="ring_a")
92+
self._write_repo(root / "WarningRepo", "2026.07.2", "b" * 40, tier="strategy-lib", ring="ring_b", enforce_bundle=False)
93+
self._write_repo(root / "CleanRepo", "2026.07.2", "37c81901160c5b31127a27dba1c63944933fb6bf", tier="pipeline", ring="ring_c")
9494

9595
buf = io.StringIO()
9696
with patch.object(qslctl, "_is_quant_repo", return_value=True), contextlib.redirect_stdout(buf):

qsl.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[qsl]
22
# QuantRuntimeSettings 作为 QSL 中央仓库
33
repo = "QuantRuntimeSettings"
4-
bundle = "2026.07.0"
5-
compat = "2026.07.0"
4+
bundle = "2026.07.2"
5+
compat = "2026.07.2"
66
tier = "ops/tooling"
77
upgrade_ring = "ring_e"
88
allow_legacy = false

scripts/check_qsl_compat.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import re
99
from dataclasses import dataclass
10+
from datetime import date
1011
from pathlib import Path
1112
from typing import Any
1213

@@ -65,6 +66,13 @@ def _get_enforce_bundle(config: dict[str, Any]) -> bool:
6566
return bool(config.get("enforce_bundle", True))
6667

6768

69+
def _compat_value(config: dict[str, Any], name: str, default: str = "") -> str:
70+
compat = config.get("compat")
71+
if isinstance(compat, dict) and name in compat:
72+
return str(compat.get(name, default)).strip()
73+
return str(config.get(name, default)).strip()
74+
75+
6876
def _string_list(value: Any) -> list[str]:
6977
if not isinstance(value, list):
7078
return []
@@ -92,6 +100,9 @@ def _load_qsl_config(repo_root: Path) -> dict[str, str | bool | list[str]]:
92100
allow_legacy = bool(config.get("allow_legacy", False))
93101
legacy_reason = str(config.get("legacy_reason", "")).strip()
94102
enforce_bundle = _get_enforce_bundle(config)
103+
exception_owner = _compat_value(config, "owner")
104+
exception_expires_at = _compat_value(config, "expires_at")
105+
exception_next_action = _compat_value(config, "next_action")
95106
live_constraint_files = _string_list(config.get("live_constraint_files"))
96107
compat = config.get("compat")
97108
if isinstance(compat, dict):
@@ -104,6 +115,9 @@ def _load_qsl_config(repo_root: Path) -> dict[str, str | bool | list[str]]:
104115
"allow_legacy": allow_legacy,
105116
"legacy_reason": legacy_reason,
106117
"enforce_bundle": enforce_bundle,
118+
"owner": exception_owner,
119+
"expires_at": exception_expires_at,
120+
"next_action": exception_next_action,
107121
"live_constraint_files": sorted(set(live_constraint_files)),
108122
"qsl_path": qsl_path.as_posix(),
109123
}
@@ -184,6 +198,9 @@ def _check(repo_root: Path, compat_root: Path) -> tuple[bool, list[str], list[st
184198
allow_legacy = bool(qsl_cfg["allow_legacy"])
185199
enforce_bundle = bool(qsl_cfg["enforce_bundle"])
186200
legacy_reason = str(qsl_cfg["legacy_reason"])
201+
exception_owner = str(qsl_cfg["owner"])
202+
exception_expires_at = str(qsl_cfg["expires_at"])
203+
exception_next_action = str(qsl_cfg["next_action"])
187204
live_constraint_files = set(qsl_cfg["live_constraint_files"]) if isinstance(qsl_cfg["live_constraint_files"], list) else set()
188205
qsl_path = str(qsl_cfg["qsl_path"])
189206

@@ -194,9 +211,23 @@ def _check(repo_root: Path, compat_root: Path) -> tuple[bool, list[str], list[st
194211
notes.append(f"enforce_bundle={enforce_bundle}")
195212
if legacy_reason:
196213
notes.append("legacy_reason=" + legacy_reason)
214+
if exception_owner:
215+
notes.append("owner=" + exception_owner)
216+
if exception_expires_at:
217+
notes.append("expires_at=" + exception_expires_at)
218+
if exception_next_action:
219+
notes.append("next_action=" + exception_next_action)
197220
if live_constraint_files:
198221
notes.append("live_constraint_files=" + ",".join(sorted(live_constraint_files)))
199222

223+
if not enforce_bundle:
224+
_validate_bundle_exception_metadata(
225+
owner=exception_owner,
226+
expires_at=exception_expires_at,
227+
next_action=exception_next_action,
228+
warnings=warnings,
229+
)
230+
200231
bundle_refs = _load_bundle(compat_root, bundle)
201232

202233
if not allow_legacy:
@@ -232,6 +263,24 @@ def _check(repo_root: Path, compat_root: Path) -> tuple[bool, list[str], list[st
232263
return (len(issues) == 0, issues, warnings, notes)
233264

234265

266+
def _validate_bundle_exception_metadata(*, owner: str, expires_at: str, next_action: str, warnings: list[str]) -> None:
267+
missing = [
268+
field
269+
for field, value in (("owner", owner), ("expires_at", expires_at), ("next_action", next_action))
270+
if not value
271+
]
272+
if missing:
273+
warnings.append("enforce_bundle=false missing exception metadata: " + ", ".join(missing))
274+
if expires_at:
275+
try:
276+
expiry = date.fromisoformat(expires_at)
277+
except ValueError:
278+
warnings.append("enforce_bundle=false expires_at must use YYYY-MM-DD")
279+
else:
280+
if expiry < date.today():
281+
warnings.append(f"enforce_bundle=false exception expired on {expires_at}")
282+
283+
235284
def _validate_ref(pin: GitRef, expected_ref: str, issues: list[str], warnings: list[str], enforce_bundle: bool) -> None:
236285
if _is_main_ref(pin.ref):
237286
issues.append(f"forbidden ref 'main' in {pin.source}:{pin.line_no}: {pin.repo}")

0 commit comments

Comments
 (0)