|
| 1 | +# === CHECKS === |
| 2 | +# id: check_full_construct_identity_reuse |
| 3 | +# proves: full_construct_has_one_character_identity, full_construct_has_one_word_identity |
| 4 | +# call: self::test_one_character_and_one_word_identity_are_reused |
| 5 | +# requires: python3 |
| 6 | +# timeout: 30 |
| 7 | +# mutates: filesystem |
| 8 | +# cleanup: tempdir_teardown |
| 9 | +# |
| 10 | +# id: check_full_construct_definition_origin_and_evidence |
| 11 | +# proves: full_construct_definitions_share_word_origin, full_construct_keeps_probability_evidence_distinct |
| 12 | +# call: self::test_definitions_share_origin_and_preserve_distinct_evidence |
| 13 | +# requires: python3 |
| 14 | +# timeout: 30 |
| 15 | +# mutates: filesystem |
| 16 | +# cleanup: tempdir_teardown |
| 17 | +# |
| 18 | +# id: check_full_construct_no_evidence_promotion |
| 19 | +# proves: full_construct_does_not_promote_evidence_to_gonols, full_construct_never_invents_geometry |
| 20 | +# call: self::test_no_old_singleton_atlas_or_invented_geometry_exists |
| 21 | +# requires: python3 |
| 22 | +# timeout: 30 |
| 23 | +# mutates: filesystem |
| 24 | +# cleanup: tempdir_teardown |
| 25 | +# |
| 26 | +# id: check_full_construct_logical_replay |
| 27 | +# proves: full_construct_replays_logically |
| 28 | +# call: self::test_logical_receipt_is_deterministic_and_detects_tamper |
| 29 | +# requires: python3 |
| 30 | +# timeout: 30 |
| 31 | +# mutates: filesystem |
| 32 | +# cleanup: tempdir_teardown |
| 33 | +# === END CHECKS === |
| 34 | + |
| 35 | +from __future__ import annotations |
| 36 | + |
| 37 | +import json |
| 38 | +import sqlite3 |
| 39 | +from pathlib import Path |
| 40 | + |
| 41 | +import pytest |
| 42 | + |
| 43 | +from english_gonol.full_construct_run import ( |
| 44 | + GEOMETRY_STATE, |
| 45 | + HMMM, |
| 46 | + RESOLUTION_RULE, |
| 47 | + SCHEMA, |
| 48 | + VERSION, |
| 49 | + FullConstructError, |
| 50 | + build_construct, |
| 51 | + verify_replay, |
| 52 | +) |
| 53 | +from english_gonol.language.source import ( |
| 54 | + LexemeRecord, |
| 55 | + SenseRecord, |
| 56 | + SynsetRecord, |
| 57 | + WordnetSnapshot, |
| 58 | +) |
| 59 | + |
| 60 | + |
| 61 | +def _snapshot() -> WordnetSnapshot: |
| 62 | + # The intentionally non-lexicographic sense order is load-bearing evidence. |
| 63 | + return WordnetSnapshot( |
| 64 | + lexemes=( |
| 65 | + LexemeRecord( |
| 66 | + lemma="alpha", |
| 67 | + part_of_speech="n", |
| 68 | + forms=(), |
| 69 | + senses=( |
| 70 | + SenseRecord( |
| 71 | + sense_id="z-sense", |
| 72 | + synset_id="s-z", |
| 73 | + relations=(("also", ("other-sense",)),), |
| 74 | + ), |
| 75 | + SenseRecord( |
| 76 | + sense_id="a-sense", |
| 77 | + synset_id="s-a", |
| 78 | + relations=(), |
| 79 | + ), |
| 80 | + ), |
| 81 | + ), |
| 82 | + LexemeRecord( |
| 83 | + lemma="other", |
| 84 | + part_of_speech="n", |
| 85 | + forms=(), |
| 86 | + senses=( |
| 87 | + SenseRecord( |
| 88 | + sense_id="other-sense", |
| 89 | + synset_id="s-other", |
| 90 | + relations=(), |
| 91 | + ), |
| 92 | + ), |
| 93 | + ), |
| 94 | + ), |
| 95 | + synsets=( |
| 96 | + SynsetRecord( |
| 97 | + synset_id="s-a", |
| 98 | + part_of_speech="n", |
| 99 | + members=("alpha",), |
| 100 | + definitions=("second alpha",), |
| 101 | + relations=(), |
| 102 | + ), |
| 103 | + SynsetRecord( |
| 104 | + synset_id="s-other", |
| 105 | + part_of_speech="n", |
| 106 | + members=("other",), |
| 107 | + definitions=("another word",), |
| 108 | + relations=(), |
| 109 | + ), |
| 110 | + SynsetRecord( |
| 111 | + synset_id="s-z", |
| 112 | + part_of_speech="n", |
| 113 | + members=("alpha", "other"), |
| 114 | + definitions=("alpha letter alpha.",), |
| 115 | + relations=(("similar", ("s-other",)),), |
| 116 | + ), |
| 117 | + ), |
| 118 | + source_tree_sha256="0" * 64, |
| 119 | + source_file_count=3, |
| 120 | + ) |
| 121 | + |
| 122 | + |
| 123 | +def _public_position(scalar: str) -> int | None: |
| 124 | + # Deterministic fixture only; this test is about English construction, |
| 125 | + # not the production UCNS authority verification performed by run(). |
| 126 | + return ord(scalar) % 157 |
| 127 | + |
| 128 | + |
| 129 | +def _build(path: Path) -> dict: |
| 130 | + return build_construct( |
| 131 | + _snapshot(), |
| 132 | + db_path=path, |
| 133 | + public_position=_public_position, |
| 134 | + ) |
| 135 | + |
| 136 | + |
| 137 | +def _table_names(db: sqlite3.Connection) -> set[str]: |
| 138 | + return { |
| 139 | + row[0] |
| 140 | + for row in db.execute( |
| 141 | + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" |
| 142 | + ) |
| 143 | + } |
| 144 | + |
| 145 | + |
| 146 | +def test_one_character_and_one_word_identity_are_reused(tmp_path: Path) -> None: |
| 147 | + db_path = tmp_path / "construct.db" |
| 148 | + _build(db_path) |
| 149 | + db = sqlite3.connect(db_path) |
| 150 | + try: |
| 151 | + alpha_id = db.execute( |
| 152 | + "SELECT id FROM words WHERE surface='alpha'" |
| 153 | + ).fetchone()[0] |
| 154 | + assert db.execute( |
| 155 | + "SELECT COUNT(*) FROM words WHERE surface='alpha'" |
| 156 | + ).fetchone()[0] == 1 |
| 157 | + |
| 158 | + # The same alpha word is both the definition origin and a participant |
| 159 | + # in its own sentence context; no occurrence-specific word is minted. |
| 160 | + origin_ids = { |
| 161 | + row[0] |
| 162 | + for row in db.execute( |
| 163 | + "SELECT DISTINCT origin_word_id FROM definitions WHERE sense_id IN ('z-sense','a-sense')" |
| 164 | + ) |
| 165 | + } |
| 166 | + assert origin_ids == {alpha_id} |
| 167 | + assert db.execute( |
| 168 | + "SELECT COUNT(*) FROM definition_words WHERE word_id=?", |
| 169 | + (alpha_id,), |
| 170 | + ).fetchone()[0] >= 3 |
| 171 | + |
| 172 | + letter_id = db.execute( |
| 173 | + "SELECT id FROM words WHERE surface='letter'" |
| 174 | + ).fetchone()[0] |
| 175 | + character_ids = [ |
| 176 | + row[0] |
| 177 | + for row in db.execute( |
| 178 | + "SELECT character_id FROM word_characters WHERE word_id=? ORDER BY ordinal", |
| 179 | + (letter_id,), |
| 180 | + ) |
| 181 | + ] |
| 182 | + # l e t t e r: repeated e and t positions reuse one character identity. |
| 183 | + assert character_ids[1] == character_ids[4] |
| 184 | + assert character_ids[2] == character_ids[3] |
| 185 | + assert db.execute( |
| 186 | + "SELECT COUNT(*) FROM characters WHERE scalar='t'" |
| 187 | + ).fetchone()[0] == 1 |
| 188 | + finally: |
| 189 | + db.close() |
| 190 | + |
| 191 | + |
| 192 | +def test_definitions_share_origin_and_preserve_distinct_evidence(tmp_path: Path) -> None: |
| 193 | + db_path = tmp_path / "construct.db" |
| 194 | + _build(db_path) |
| 195 | + db = sqlite3.connect(db_path) |
| 196 | + try: |
| 197 | + alpha_id = db.execute( |
| 198 | + "SELECT id FROM words WHERE surface='alpha'" |
| 199 | + ).fetchone()[0] |
| 200 | + definitions = db.execute( |
| 201 | + "SELECT id, origin_word_id, ordinal, sense_id, text " |
| 202 | + "FROM definitions WHERE origin_word_id=? ORDER BY ordinal", |
| 203 | + (alpha_id,), |
| 204 | + ).fetchall() |
| 205 | + assert [(row[2], row[3]) for row in definitions] == [ |
| 206 | + (1, "z-sense"), |
| 207 | + (2, "a-sense"), |
| 208 | + ] |
| 209 | + assert all(row[1] == alpha_id for row in definitions) |
| 210 | + |
| 211 | + first_id = definitions[0][0] |
| 212 | + spans = db.execute( |
| 213 | + "SELECT ordinal, w.surface, start_offset, end_offset " |
| 214 | + "FROM definition_words dw JOIN words w ON w.id=dw.word_id " |
| 215 | + "WHERE definition_id=? ORDER BY ordinal", |
| 216 | + (first_id,), |
| 217 | + ).fetchall() |
| 218 | + assert spans == [ |
| 219 | + (1, "alpha", 0, 5), |
| 220 | + (2, "letter", 6, 12), |
| 221 | + (3, "alpha", 13, 18), |
| 222 | + ] |
| 223 | + |
| 224 | + other_id = db.execute( |
| 225 | + "SELECT id FROM words WHERE surface='other'" |
| 226 | + ).fetchone()[0] |
| 227 | + evidence = db.execute( |
| 228 | + "SELECT channel, relation, target_word_id, target_ref " |
| 229 | + "FROM semantic_evidence WHERE definition_id=? ORDER BY source_ordinal, target_word_id", |
| 230 | + (first_id,), |
| 231 | + ).fetchall() |
| 232 | + assert ("sense", "also", other_id, "other-sense") in evidence |
| 233 | + assert ("synset", "similar", other_id, "s-other") in evidence |
| 234 | + assert ("synset-membership", "co-member", other_id, "other") in evidence |
| 235 | + |
| 236 | + meta = dict(db.execute("SELECT key, value FROM meta")) |
| 237 | + assert meta["geometry_state"] == GEOMETRY_STATE |
| 238 | + assert meta["resolution_rule"] == RESOLUTION_RULE |
| 239 | + assert json.loads(meta["evidence_channels"]) == [ |
| 240 | + "ordinal", |
| 241 | + "semantic", |
| 242 | + "sentence-context", |
| 243 | + ] |
| 244 | + assert HMMM in meta["hmmm"] |
| 245 | + finally: |
| 246 | + db.close() |
| 247 | + |
| 248 | + |
| 249 | +def test_no_old_singleton_atlas_or_invented_geometry_exists(tmp_path: Path) -> None: |
| 250 | + db_path = tmp_path / "construct.db" |
| 251 | + _build(db_path) |
| 252 | + db = sqlite3.connect(db_path) |
| 253 | + try: |
| 254 | + assert _table_names(db) == { |
| 255 | + "meta", |
| 256 | + "characters", |
| 257 | + "words", |
| 258 | + "word_characters", |
| 259 | + "definitions", |
| 260 | + "definition_words", |
| 261 | + "semantic_evidence", |
| 262 | + "unresolved_semantic_evidence", |
| 263 | + } |
| 264 | + forbidden_fragments = { |
| 265 | + "weight", |
| 266 | + "vector", |
| 267 | + "coordinate", |
| 268 | + "center", |
| 269 | + "radius", |
| 270 | + "tangent", |
| 271 | + "motion", |
| 272 | + } |
| 273 | + for table in _table_names(db): |
| 274 | + columns = { |
| 275 | + row[1].lower() |
| 276 | + for row in db.execute(f"PRAGMA table_info({table})") |
| 277 | + } |
| 278 | + assert not any( |
| 279 | + fragment in column |
| 280 | + for column in columns |
| 281 | + for fragment in forbidden_fragments |
| 282 | + ) |
| 283 | + |
| 284 | + # Sense and synset identifiers are provenance fields on definitions; |
| 285 | + # they never become independent object tables. |
| 286 | + assert "senses" not in _table_names(db) |
| 287 | + assert "synsets" not in _table_names(db) |
| 288 | + assert "sentences" not in _table_names(db) |
| 289 | + assert "occurrences" not in _table_names(db) |
| 290 | + assert "relation_circles" not in _table_names(db) |
| 291 | + assert "tangencies" not in _table_names(db) |
| 292 | + finally: |
| 293 | + db.close() |
| 294 | + |
| 295 | + |
| 296 | +def test_logical_receipt_is_deterministic_and_detects_tamper(tmp_path: Path) -> None: |
| 297 | + first_dir = tmp_path / "first" |
| 298 | + second_dir = tmp_path / "second" |
| 299 | + first_dir.mkdir() |
| 300 | + second_dir.mkdir() |
| 301 | + |
| 302 | + first = _build(first_dir / "construct.db") |
| 303 | + second = _build(second_dir / "construct.db") |
| 304 | + assert first["receipt_sha256"] == second["receipt_sha256"] |
| 305 | + |
| 306 | + manifest = { |
| 307 | + "schema": SCHEMA, |
| 308 | + "version": VERSION, |
| 309 | + "construct_file": "construct.db", |
| 310 | + "receipt_sha256": first["receipt_sha256"], |
| 311 | + } |
| 312 | + (first_dir / "manifest.json").write_text( |
| 313 | + json.dumps(manifest, sort_keys=True) + "\n", |
| 314 | + encoding="utf-8", |
| 315 | + ) |
| 316 | + assert verify_replay(first_dir)["receipt_sha256"] == first["receipt_sha256"] |
| 317 | + |
| 318 | + db = sqlite3.connect(first_dir / "construct.db") |
| 319 | + try: |
| 320 | + db.execute("UPDATE words SET surface='tampered' WHERE surface='alpha'") |
| 321 | + db.commit() |
| 322 | + finally: |
| 323 | + db.close() |
| 324 | + |
| 325 | + with pytest.raises(FullConstructError, match="receipt mismatch"): |
| 326 | + verify_replay(first_dir) |
0 commit comments