-
Notifications
You must be signed in to change notification settings - Fork 0
fix: resolve three lacuna-dogfood defects (posture crash, env mint-drop, fingerprint fragility) — v1.1.0 #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| """Legis — the git/CI + governance layer of the Weft suite.""" | ||
|
|
||
| __version__ = "1.0.0" | ||
| __version__ = "1.1.0" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ | |
| Text, | ||
| create_engine, | ||
| insert, | ||
| inspect, | ||
| select, | ||
| text, | ||
| ) | ||
|
|
@@ -310,9 +311,22 @@ def append_signed(self, build_payload: BuildSignedPayload) -> int: | |
| conn.execute(text("BEGIN IMMEDIATE")) | ||
| return self._insert_signed(conn, build_payload) | ||
|
|
||
| def _has_log_table(self, conn) -> bool: | ||
| """True iff the ``audit_log`` table exists on this connection. | ||
|
|
||
| An ``initialize=False`` handle opened against an unprovisioned DB (a | ||
| missing or empty/no-table file) has no table; reads then treat the store | ||
| as empty rather than raising ``OperationalError("no such table")`` | ||
| (fail-closed: callers map an empty store to their safe default). DDL only | ||
| ever runs under ``initialize=True``, so a read can never create it. | ||
| """ | ||
| return inspect(conn).has_table("audit_log") | ||
|
|
||
| def read_all(self) -> list[AuditRecord]: | ||
| self._assert_no_batch_in_progress("read_all") | ||
| with self._engine.begin() as conn: | ||
| if not self._has_log_table(conn): | ||
| return [] | ||
|
Comment on lines
+328
to
+329
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When any initialized governance/signoff/binding audit database is pointed at an empty file or has its Useful? React with 👍 / 👎. |
||
| rows = conn.execute( | ||
| select(self._log).order_by(self._log.c.seq.asc()) | ||
| ).all() | ||
|
|
@@ -330,6 +344,8 @@ def read_all(self) -> list[AuditRecord]: | |
| def read_by_seq(self, seq: int) -> AuditRecord | None: | ||
| self._assert_no_batch_in_progress("read_by_seq") | ||
| with self._engine.begin() as conn: | ||
| if not self._has_log_table(conn): | ||
| return None | ||
| row = conn.execute( | ||
| select(self._log).where(self._log.c.seq == seq) | ||
| ).first() | ||
|
|
@@ -428,6 +444,8 @@ def verify_integrity(self) -> bool: | |
| def get_latest_sequence_and_hash(self) -> tuple[int, str]: | ||
| self._assert_no_batch_in_progress("get_latest_sequence_and_hash") | ||
| with self._engine.begin() as conn: | ||
| if not self._has_log_table(conn): | ||
| return 0, GENESIS | ||
| row = conn.execute( | ||
| select(self._log.c.seq, self._log.c.chain_hash) | ||
| .order_by(self._log.c.seq.desc()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| """The policy-boundary fingerprint must be stable across Python interpreters. | ||
|
|
||
| dogfood: legis-13b4e97bf4. The fingerprint canonicalization used to be a raw | ||
| ``ast.dump`` hash, whose text is Python-version-dependent: 3.13 omits | ||
| default-empty AST fields (an empty ``arguments`` node) where 3.12 lists | ||
| ``posonlyargs`` / ``args`` / ``kwonlyargs`` etc. A fingerprint pinned under one | ||
| interpreter then reports a spurious mismatch under another. The fix replaces the | ||
| ``ast.dump`` text with a serializer that emits EVERY field of every node in a | ||
| fixed order, so empty fields cannot be silently dropped — version-stable by | ||
| construction. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from legis.policy.decorator import fingerprint_source, get_normalized_ast_str | ||
|
|
||
| _REPO_ROOT = Path(__file__).resolve().parents[2] | ||
| _SRC = _REPO_ROOT / "src" | ||
|
|
||
| # A snippet whose AST has an ``arguments`` node with several empty subfields — | ||
| # exactly the shape that differs between ``ast.dump`` on 3.12 vs 3.13. | ||
| _SNIPPET = "def t():\n assert True\n" | ||
|
|
||
|
|
||
| def test_normalized_ast_str_emits_empty_fields(): | ||
| # The version-stability invariant: empty argument-list fields are emitted | ||
| # explicitly, never dropped (raw ast.dump on 3.13 omits these). This is the | ||
| # property that neutralizes the interpreter difference. | ||
| dumped = get_normalized_ast_str(_SNIPPET) | ||
| for field in ("posonlyargs=[]", "args=[]", "kwonlyargs=[]", "defaults=[]"): | ||
| assert field in dumped, f"{field!r} missing from {dumped!r}" | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| shutil.which("python3.12") is None, reason="needs python3.12 for cross-interp check" | ||
| ) | ||
| def test_fingerprint_identical_across_python_minor(): | ||
| # Decisive end-to-end proof: the repo's OWN fingerprint over one fixed source | ||
| # must be byte-identical under the in-process interpreter and under | ||
| # python3.12. Before the fix these differed; after it they match. | ||
| here_fp = fingerprint_source(_SNIPPET) | ||
|
|
||
| prog = ( | ||
| "import sys\n" | ||
| f"sys.path.insert(0, {str(_SRC)!r})\n" | ||
| "from legis.policy.decorator import fingerprint_source\n" | ||
| f"sys.stdout.write(fingerprint_source({_SNIPPET!r}))\n" | ||
| ) | ||
| other = subprocess.run( | ||
| ["python3.12", "-c", prog], | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| assert other.returncode == 0, other.stderr | ||
| assert other.stdout == here_fp, ( | ||
| f"fingerprint drifted across interpreters: " | ||
| f"{sys.version_info[:2]} -> {here_fp}, 3.12 -> {other.stdout}" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this walks the current interpreter's
node._fields, fields added in newer Python minors still change the serialized text. I checked a source valid on the supported floor,def test_generic[T](): ...: Python 3.12'sast.TypeVar._fieldsis('name', 'bound'), while 3.13 addsdefault_value, so this serializer emitsdefault_value=Noneonly on 3.13 and the fingerprints differ. Any pinned boundary evidence test using PEP-695 type parameters will still report drift after a 3.12→3.13 upgrade.Useful? React with 👍 / 👎.