stp-protocol 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1558 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ═══════════════════════════════════════════════════════════════════
4
+ SOVEREIGN TRACE PROTOCOL — STAMP FUNCTION
5
+ ═══════════════════════════════════════════════════════════════════
6
+
7
+ Version: FROZEN-4.0
8
+ Date: June 1, 2026
9
+ Co-authors: Sheldon K. Salmon — AI Reliability Architect
10
+ ALBEDO (Claude, Anthropic)
11
+ Stack: DUAL-HELIX v2.0 · TOPOS v0.4 · VELA-C v0.3
12
+ Constitutional: SOVEREIGNTY_STACK.md — Laws 1–8 active | Law 9 dark
13
+ ORCID: 0009-0005-8057-5115
14
+
15
+ FROZEN DECLARATION
16
+ ──────────────────
17
+ This file is written once, verified once, deployed permanently.
18
+ No patches. No updates. No deprecation.
19
+ The stamp it generates is only permanent if the code that generates
20
+ it is also permanent. Modification of this file invalidates the
21
+ integrity guarantee of all stamps produced after modification.
22
+
23
+ If a defect is found: retire this file, archive it as
24
+ SOVEREIGN-TRACE-STAMP-FROZEN-4.0-RETIRED, and create
25
+ FROZEN-5.0 from scratch with documented correction.
26
+ Do not patch. Do not edit.
27
+
28
+ INTEGRITY BOUNDARIES (elevated from _compute_seal — PDE F-005)
29
+ ────────────────────────────────────────────────────────────────
30
+ The SHA-256 seal proves that a specific entry text was bound to
31
+ specific calendar strings at a specific unix time by this version
32
+ of the stamp function.
33
+
34
+ The seal does NOT independently prove that the calendar strings
35
+ are correctly computed. That guarantee is carried by the self-test
36
+ and the algorithm specification. If this file is modified such that
37
+ _gregorian(), _hebrew(), or _dreamspell() produce wrong output,
38
+ the seal will still be cryptographically valid — it will correctly
39
+ bind wrong calendar strings to the entry.
40
+
41
+ This distinction matters: seal verification confirms integrity of
42
+ the payload record. It does not re-run the calendar algorithms.
43
+ Operators who need to verify calendar correctness must run the
44
+ self-test, not just verify().
45
+
46
+ ABSTRACTION BARGAIN DECLARATION (FA v4.0 P-20 / Sub-factor G)
47
+ ──────────────────────────────────────────────────────────────
48
+ This module trades purity for usability in one location only:
49
+ stamp(entry_text, dt=None) — when dt is None, calls
50
+ datetime.now(timezone.utc) internally (Class 6 I/O side-effect).
51
+ The bargain: callers who need full determinism must supply dt.
52
+ Callers who need convenience may omit it.
53
+ All other public functions are referentially transparent.
54
+ The impurity is isolated, documented, and testable via dt injection.
55
+ The dt injection pattern is the correct engineering resolution:
56
+ it makes the impure path narrow and the pure path the default
57
+ for testing.
58
+
59
+ TEMPORAL CONSISTENCY NOTE (PDE F-020)
60
+ ──────────────────────────────────────
61
+ stamp() called directly in a loop without dt will call
62
+ datetime.now(timezone.utc) independently for each entry. Two
63
+ sequential stamp() calls that straddle a UTC midnight will produce
64
+ entries with different gregorian, hebrew, and dreamspell values
65
+ despite being stamped within seconds of each other. This is
66
+ correct behavior — each stamp reflects the moment it was called.
67
+ Callers requiring temporal consistency across multiple entries
68
+ should use stamp_batch() or supply an explicit dt to each call.
69
+
70
+ FROZEN-5.0 CANDIDATES (PDE F-015 — append-only during FROZEN-4.0 lifetime)
71
+ ────────────────────────────────────────────────────────────────────────────
72
+ CANDIDATE-01 (PDE F-009): _dreamspell() defensive guard returns
73
+ "Day Out of Time" on delta >= 364 branch — structurally unreachable
74
+ but if reached (via future refactoring), should raise ValueError
75
+ with a diagnostic message rather than returning a plausible-wrong
76
+ string. Change to: raise ValueError("Dreamspell delta overflow —
77
+ algorithm error. Create FROZEN-5.0.")
78
+
79
+ CANDIDATE-02 (PDE F-006): stamp() on dates before Jan 1, 1970 UTC
80
+ produces a negative or zero unix_utc via int(dt.timestamp())
81
+ truncation. from_dict() enforces unix_utc > 0 and catches this at
82
+ deserialization, but stamp() itself does not guard against it.
83
+ Consider adding: if unix_utc <= 0: raise ValueError("unix_utc
84
+ must be positive — pre-1970 UTC dates are not supported.")
85
+
86
+ NOTE — RESOLVED IN 4.0 (adversarial battery June 2026):
87
+ Invisible character guard added to _validate_entry_text():
88
+ stamp("\u200b") and stamp("\x00") now raise ValueError.
89
+ This hardening was applied after initial FROZEN-4.0 release and
90
+ before the sentinel hash was set. No FROZEN-3.0-or-earlier behavior
91
+ is changed. The fix is a pre-seal hardening, not a post-seal patch.
92
+
93
+ CANDIDATE-03 (PDE F-025): verify() should check ts.version against
94
+ _FROZEN_VERSION before attempting seal recomputation and return
95
+ VerifyResult(False, "VERSION_MISMATCH") if they differ, to
96
+ distinguish tampered content from wrong frozen version used.
97
+
98
+ CANDIDATE-04 (PDE F-019): Self-test failure output (the failures
99
+ list print before AssertionError) should go to stderr, not stdout,
100
+ so it does not corrupt piped JSON output in --json mode.
101
+
102
+ CANDIDATE-05 (PDE F-013): _MAX_YEAR_SEARCH = 6 "within 3 years"
103
+ claim should carry a specific D&R page reference or be tagged [S]
104
+ with "empirically verified over [1582, 4000]."
105
+
106
+ CANDIDATE-06 (MF-M-003): CLI "This stamp is permanent." should
107
+ be qualified: "This stamp's seal is computationally tamper-evident
108
+ under SHA-256." to resolve the permanent/tamper-evident slippage.
109
+
110
+ FROZEN-3.0 RETIREMENT NOTE
111
+ ───────────────────────────
112
+ FROZEN-3.0 is retired. One CRITICAL finding and seven additional
113
+ MEDIUM/LOW findings identified in FSVE v4.3 × FA v4.0 full red team
114
+ scan (scan date: June 1, 2026). Primary defect:
115
+
116
+ 1. Self-test anchor dates for RH 5787 and Erev RH 5787 were wrong.
117
+ The algorithm was correct; the test data was not.
118
+ Effect: self-test raises AssertionError on otherwise correct code.
119
+ Correct values: RH 5787 = Sep 12, 2026 | Erev = Sep 11, 2026.
120
+ Root cause: test written with Sep 22 as RH 5787 — calendar lookup
121
+ error during extended anchor verification session.
122
+
123
+ Additional MEDIUM findings corrected in FROZEN-4.0:
124
+
125
+ 2. Pre-reform date boundary: dates in 1582 before Oct 15 were accepted
126
+ silently. Fixed with explicit month/day guard in _jd_from_gregorian.
127
+
128
+ 3. version field in from_dict() was not validated against _FROZEN_VERSION.
129
+ Fixed with explicit equality check on deserialization — a dict carrying
130
+ a foreign version string now raises ValueError rather than silently
131
+ constructing a stamp with a wrong provenance claim.
132
+
133
+ 4. Abstraction Bargain Declaration absent from module docstring.
134
+ Added per FA v4.0 P-20 / Sub-factor G (this block above).
135
+
136
+ LOW findings corrected in FROZEN-4.0:
137
+
138
+ 5. check_invariant() docstring clarified: function checks structural
139
+ invariants only — does not verify seal against entry.
140
+ 6. stamp_batch() atomicity behavior clarified in docstring.
141
+ 7. CLI privacy notice extended with process-memory note.
142
+ 8. _FROZEN_FILE_SHA256 sentinel operator verification procedure
143
+ clarified in docstring.
144
+
145
+ All FROZEN-3.0 stamps remain cryptographically valid and verifiable
146
+ using FROZEN-3.0 (archived as SOVEREIGN-TRACE-STAMP-FROZEN-3.0-RETIRED.py).
147
+ The algorithm defect was in test data only — no stamp produced by
148
+ FROZEN-3.0 is cryptographically incorrect.
149
+
150
+ FROZEN-2.0 RETIREMENT NOTE
151
+ ───────────────────────────
152
+ FROZEN-2.0 is retired. Eight HIGH findings and 22 additional
153
+ MEDIUM/LOW findings identified in full PDE v0.5 · HDRE v0.3 ·
154
+ CAL v0.3 scan (scan date: June 1, 2026). Primary defects:
155
+
156
+ 1. No Unicode normalization (NFC) on entry_text before sealing.
157
+ 2. No stamp version field in seal payload.
158
+ 3. Year length not validated against legal Hebrew year lengths.
159
+ 4. No convergence bound on Hebrew year-finding loop.
160
+ 5. from_dict() accepted unvalidated dicts.
161
+ 6. Whitespace sealed but not normalized.
162
+ 7. int(dt.timestamp()) truncation undeclared.
163
+ 8. _jd_from_gregorian() accepted pre-Gregorian-reform years silently.
164
+
165
+ All FROZEN-2.0 stamps carry no version field in their seal —
166
+ this is the definitive retirement marker.
167
+
168
+ FROZEN-1.0 RETIREMENT NOTE (carried forward)
169
+ ─────────────────────────────────────────────
170
+ FROZEN-1.0 is retired. Defect: Hebrew calendar omitted the four
171
+ dehiyot (postponement rules) correctly. Archived as:
172
+ SOVEREIGN-TRACE-STAMP-FROZEN-1.0-RETIRED.py
173
+
174
+ BACKWARD COMPATIBILITY
175
+ ──────────────────────
176
+ FROZEN-4.0 seals are NOT backward-compatible with earlier versions.
177
+ The seal payload includes "version":"FROZEN-4.0".
178
+ Use the matching archived frozen version to verify older stamps.
179
+
180
+ CROSS-VERSION VERIFICATION (PDE F-011, F-025, XD-002)
181
+ ──────────────────────────────────────────────────────
182
+ verify() in this file will return SEAL_MISMATCH (not VERSION_MISMATCH)
183
+ when used to verify a stamp produced by an earlier frozen version.
184
+ This is because the version string is baked into the seal payload.
185
+ If you receive a SEAL_MISMATCH result on a record you trust:
186
+ 1. Check the record's "version" field.
187
+ 2. If it is not "FROZEN-4.0", retrieve the corresponding archived
188
+ frozen version from:
189
+ sovereign_trace/FROZEN-[N].0-RETIRED/
190
+ 3. Use that archived version's verify() to confirm the record.
191
+ SEAL_MISMATCH ≠ tampered content. It may mean wrong version used.
192
+ This distinction will be resolved with a VERSION_MISMATCH reason
193
+ code in FROZEN-5.0 (see FROZEN-5.0 CANDIDATES above).
194
+
195
+ VELA-C v0.3 COMPLIANCE
196
+ ───────────────────────
197
+ ✓ Zero external dependencies (stdlib only: hashlib, json, datetime,
198
+ unicodedata, re)
199
+ ✓ Single epistemic function: generate a triple-time seal
200
+ ✓ No exception handler returning CLEAN silently
201
+ ✓ Attribution header complete
202
+ ✓ Self-test with verified anchor cases (Hebcal/Chabad-verified)
203
+ Extended: leap year, long Cheshvan, short Kislev, Adar II
204
+ FROZEN-4.0: RH 5787 anchor corrected to Sep 12 2026 (pyluach verified)
205
+ ✓ Frozen deployment: no __init__ mutation, no module-level singletons
206
+
207
+ WHAT THIS DOES
208
+ ──────────────
209
+ Generates a cryptographic stamp encoding a moment simultaneously in:
210
+ — Gregorian (civic time)
211
+ — Hebrew lunisolar calendar (current era)
212
+ — 13 Moon Dreamspell calendar (Argüelles system, not traditional
213
+ Maya Tzolkin/Haab — disambiguation declared)
214
+
215
+ SHA-256 binds the entry text, all three representations, and the
216
+ version identifier FROZEN-4.0 together. The seal is the proof.
217
+ The text is the content. Together: the trace.
218
+
219
+ Entry text normalization:
220
+ Entry text is NFC-normalized and stripped before sealing.
221
+ Callers providing pre-normalized text will produce identical seals.
222
+ Leading/trailing whitespace is discarded before sealing.
223
+
224
+ Timestamp precision:
225
+ unix_utc is whole seconds only (truncated, not rounded).
226
+ Sub-second precision is intentionally discarded.
227
+ Two datetimes within the same second produce identical unix_utc.
228
+
229
+ Date convention:
230
+ All calendar dates reflect UTC date at stamp time.
231
+ For users in UTC+10 or later, the UTC date may differ from
232
+ local civil date. Hebrew and Dreamspell reflect UTC date.
233
+
234
+ Valid date range:
235
+ Gregorian calendar reform: October 15, 1582.
236
+ Input dates before October 15, 1582 will raise ValueError.
237
+ Upper bound: year 4000 (practical; no algorithmic constraint).
238
+ Pre-1970 UTC dates: supported by the calendar algorithm but
239
+ unix_utc will be 0 or negative (see FROZEN-5.0 CANDIDATE-02).
240
+
241
+ HEBREW CALENDAR — ALGORITHM SPECIFICATION
242
+ ──────────────────────────────────────────
243
+ Algorithm: Dershowitz & Reingold two-pass approach.
244
+ Pass 1 — elapsed_days(): molad-based count implementing
245
+ Lo ADU Rosh (not on Sun/Wed/Fri) and Molad Zaken (>=18h).
246
+ Pass 2 — new_year_delay(): handles GaTaRaD and BeTUTeKaPoT
247
+ by checking adjacent year lengths for non-standard values
248
+ (356 or 382 days indicate postponement required).
249
+ Theorem reference: D&R "Calendrical Calculations" 4th ed.,
250
+ Chapter 8 (Hebrew Calendar), §8.1–§8.3 for elapsed_days();
251
+ §8.4 for new_year_delay() GaTaRaD/BeTUTeKaPoT conditions.
252
+ Epoch: JD 347998 — civil-day convention.
253
+ Note: Hebrew day begins at sunset. Civil-day convention assigns
254
+ the Hebrew date corresponding to the daytime hours of a civil date.
255
+ This aligns with Hebcal and Chabad.org output.
256
+ Example: Rosh Hashanah 5786 begins sundown Sep 22, 2025.
257
+ Civil-day: Sep 23, 2025 = 1 Tishri 5786. [D]
258
+
259
+ Hebrew month naming convention (PDE F-014):
260
+ In a common (non-leap) year, the single Adar month is "Adar".
261
+ In a leap year, the two months are "Adar I" and "Adar II".
262
+ "Adar I" is NEVER used in a common year. If you expected "Adar I"
263
+ for a common-year date, you are in a common year and the correct
264
+ name is "Adar".
265
+
266
+ Legal Hebrew year lengths: {353, 354, 355, 383, 384, 385}
267
+ 353/354/355 = common year (deficient/regular/complete)
268
+ 383/384/385 = leap year (deficient/regular/complete)
269
+ Any other year length is an algorithm error — raises ValueError.
270
+
271
+ ANCHOR VERIFICATION (session of origin — March 3, 2026)
272
+ ────────────────────────────────────────────────────────
273
+ Gregorian: March 3, 2026 [D]
274
+ Hebrew: 14 Adar 5786 (Purim) [D] — Hebcal/Chabad verified
275
+ Dreamspell: Day 25, Galactic Moon 8/13 [R] — verified by calculation
276
+
277
+ FILE INTEGRITY
278
+ ──────────────
279
+ _FROZEN_FILE_SHA256 is set at release time to the SHA-256 of this file
280
+ computed AFTER the sentinel value is embedded. This is the released
281
+ value — the file you deploy must hash to this exact value.
282
+
283
+ SENTINEL STATE CHECK: If _FROZEN_FILE_SHA256 == "SET_AT_RELEASE_TIME",
284
+ this is a pre-release copy. The self-test will flag this. Do not use
285
+ a pre-release copy in production. The sentinel must be set before
286
+ deployment. (PDE F-001, F-002 — sentinel visibility hardened.)
287
+
288
+ Operator deployment verification procedure:
289
+ Step 1. Obtain the released sovereign_trace_stamp.py
290
+ Step 2. Run: sha256sum sovereign_trace_stamp.py
291
+ or: python3 -c "import hashlib; print(hashlib.sha256(
292
+ open('sovereign_trace_stamp.py','rb').read()).hexdigest())"
293
+ Step 3. Compare output against _FROZEN_FILE_SHA256 below.
294
+ Step 4. If they match: the file is unmodified.
295
+ If they differ: the file has been altered since release.
296
+ Do not deploy an altered file.
297
+
298
+ The bootstrap problem (a file cannot contain its own hash before the
299
+ hash is computed) means the sentinel is inserted last, after all code
300
+ is final. The "SET_AT_RELEASE_TIME" placeholder in any pre-release
301
+ copy is expected — the released file carries the actual hash.
302
+
303
+ GENESIS SEAL
304
+ ────────────
305
+ The founding seal of the STP ledger. Sealed March 10, 2026.
306
+ Ledger entry: LEDGER-001-FOUNDING-SEAL.
307
+ This constant is provided for machine-readable provenance verification.
308
+ A valid STP installation can confirm its ledger origin by checking
309
+ that LEDGER-001 carries a seal matching this value.
310
+
311
+ ═══════════════════════════════════════════════════════════════════
312
+ """
313
+
314
+ import hashlib
315
+ import json
316
+ import re
317
+ import unicodedata
318
+ from datetime import datetime, timezone, date as _date
319
+ from typing import NamedTuple
320
+
321
+
322
+ # ═══════════════════════════════════════════════════════════════════
323
+ # GENESIS SEAL — LEDGER-001-FOUNDING-SEAL
324
+ # Sealed: March 10, 2026
325
+ # This is the cryptographic origin of the STP ledger.
326
+ # Do not modify. This value is part of the permanent provenance chain.
327
+ # ═══════════════════════════════════════════════════════════════════
328
+ _GENESIS_SEAL = "a63c4f28cf7c3f2c63c220d61980fb85211270924a7c3b3e5f230019cecb6713"
329
+ _GENESIS_LEDGER_ENTRY = "LEDGER-001-FOUNDING-SEAL"
330
+ _GENESIS_DATE_GREGORIAN = "March 10, 2026"
331
+
332
+
333
+ # ═══════════════════════════════════════════════════════════════════
334
+ # FILE INTEGRITY SEAL — FROZEN-4.0
335
+ # Set this constant to the SHA-256 of the released .py file.
336
+ # Verification is the operator's deployment responsibility.
337
+ # Internal self-hash is impossible (bootstrap problem).
338
+ #
339
+ # SENTINEL STATE: "SET_AT_RELEASE_TIME" means this is a pre-release
340
+ # copy. The self-test will warn. Do not deploy until this value
341
+ # is replaced with the actual file hash. (PDE F-001, F-002)
342
+ # ═══════════════════════════════════════════════════════════════════
343
+ _FROZEN_FILE_SHA256 = "8962b37c67897611c6358d080d8d905f6aff7a426ef610b6d63612cb80b67774"
344
+ _FROZEN_VERSION = "FROZEN-4.0"
345
+
346
+
347
+ # ═══════════════════════════════════════════════════════════════════
348
+ # STAMP DATA STRUCTURE
349
+ # ═══════════════════════════════════════════════════════════════════
350
+
351
+ class SovereignStamp:
352
+ """
353
+ Immutable triple-time cryptographic stamp.
354
+
355
+ Fields
356
+ ------
357
+ gregorian : str — "March 3, 2026"
358
+ hebrew : str — "14 Adar 5786"
359
+ dreamspell : str — "Day 25, Galactic Moon 8/13"
360
+ unix_utc : int — seconds since Unix epoch (UTC), whole seconds only
361
+ seal : str — SHA-256 hex digest binding entry, time, and version
362
+ version : str — "FROZEN-4.0" — sealed into the payload
363
+
364
+ Invariant
365
+ ---------
366
+ seal is a 64-character lowercase hex string.
367
+ unix_utc is a positive integer.
368
+ gregorian, hebrew, dreamspell, version are non-empty strings.
369
+
370
+ Note on __repr__ truncation (PDE F-016):
371
+ __repr__ truncates the seal to 16 characters for log readability.
372
+ __str__ shows the full 64-character seal.
373
+ display() and display_ascii() also show the full seal.
374
+ Use str(ts) or display(ts) when you need the complete seal.
375
+ """
376
+
377
+ __slots__ = ("gregorian", "hebrew", "dreamspell", "unix_utc", "seal", "version")
378
+
379
+ def __init__(self, gregorian, hebrew, dreamspell, unix_utc, seal,
380
+ version=_FROZEN_VERSION):
381
+ object.__setattr__(self, "gregorian", gregorian)
382
+ object.__setattr__(self, "hebrew", hebrew)
383
+ object.__setattr__(self, "dreamspell", dreamspell)
384
+ object.__setattr__(self, "unix_utc", unix_utc)
385
+ object.__setattr__(self, "seal", seal)
386
+ object.__setattr__(self, "version", version)
387
+
388
+ def __setattr__(self, *_):
389
+ raise AttributeError("SovereignStamp is immutable — frozen by design.")
390
+
391
+ def __repr__(self):
392
+ # NOTE: seal is truncated to 16 characters here for log readability.
393
+ # Use str(ts) or display(ts) for the full 64-character seal.
394
+ return (
395
+ f"SovereignStamp("
396
+ f"gregorian={self.gregorian!r}, "
397
+ f"hebrew={self.hebrew!r}, "
398
+ f"dreamspell={self.dreamspell!r}, "
399
+ f"unix_utc={self.unix_utc}, "
400
+ f"version={self.version!r}, "
401
+ f"seal={self.seal[:16]!r}...)"
402
+ )
403
+
404
+ def __str__(self):
405
+ """Full-seal string representation — distinct from __repr__ truncation."""
406
+ return (
407
+ f"SovereignStamp("
408
+ f"gregorian={self.gregorian!r}, "
409
+ f"hebrew={self.hebrew!r}, "
410
+ f"dreamspell={self.dreamspell!r}, "
411
+ f"unix_utc={self.unix_utc}, "
412
+ f"version={self.version!r}, "
413
+ f"seal={self.seal!r})"
414
+ )
415
+
416
+ def __eq__(self, other):
417
+ """Equality by seal — the canonical cryptographic identity."""
418
+ if not isinstance(other, SovereignStamp):
419
+ return NotImplemented
420
+ return self.seal == other.seal
421
+
422
+ def __hash__(self):
423
+ """Hash by seal — consistent with __eq__."""
424
+ return hash(self.seal)
425
+
426
+ def check_invariant(self) -> bool:
427
+ """
428
+ Verify structural invariants of this stamp.
429
+
430
+ Checks that all fields satisfy the SovereignStamp structural
431
+ invariant: seal is a 64-character lowercase hex string, unix_utc
432
+ is a positive integer, and all string fields are non-empty.
433
+
434
+ Does NOT verify the seal against an entry — the entry text is not
435
+ stored in SovereignStamp. Use verify(entry, stamp) to confirm
436
+ cryptographic integrity. check_invariant() is a structural health
437
+ check, not a cryptographic proof.
438
+
439
+ Does NOT check that version == _FROZEN_VERSION. A stamp object
440
+ with a foreign version string passes check_invariant() — this is
441
+ intentional. Version enforcement is the responsibility of from_dict()
442
+ at deserialization time. See PDE F-012.
443
+
444
+ Returns True if all structural invariants hold, False otherwise.
445
+ Raises nothing — returns False on any failure.
446
+ """
447
+ _SEAL_RE = re.compile(r'^[0-9a-f]{64}$')
448
+ if not (isinstance(self.seal, str) and _SEAL_RE.match(self.seal)):
449
+ return False
450
+ if not isinstance(self.unix_utc, int) or self.unix_utc <= 0:
451
+ return False
452
+ for f in ('gregorian', 'hebrew', 'dreamspell', 'version'):
453
+ v = getattr(self, f, None)
454
+ if not (isinstance(v, str) and v):
455
+ return False
456
+ return True
457
+
458
+
459
+ # ═══════════════════════════════════════════════════════════════════
460
+ # SOVEREIGN RECORD — TYPED {entry, stamp} PAIR
461
+ # ═══════════════════════════════════════════════════════════════════
462
+
463
+ class SovereignRecord:
464
+ """
465
+ Canonical {entry, stamp} pair — the minimal unit of ledger storage.
466
+
467
+ The stamp alone cannot be verified without the entry.
468
+ The entry alone has no cryptographic anchor.
469
+ The SovereignRecord is the minimal unit of sovereign trace:
470
+ it carries both the content and the proof.
471
+
472
+ Immutable — same pattern as SovereignStamp.
473
+ """
474
+ __slots__ = ("entry", "stamp")
475
+
476
+ def __init__(self, entry: str, stamp: SovereignStamp):
477
+ object.__setattr__(self, "entry", entry)
478
+ object.__setattr__(self, "stamp", stamp)
479
+
480
+ def __setattr__(self, *_):
481
+ raise AttributeError("SovereignRecord is immutable.")
482
+
483
+ def __repr__(self):
484
+ return (
485
+ f"SovereignRecord("
486
+ f"entry={self.entry[:40]!r}{'...' if len(self.entry) > 40 else ''}, "
487
+ f"stamp={self.stamp!r})"
488
+ )
489
+
490
+ def verify(self) -> "VerifyResult":
491
+ """Verify the entry against the stamp seal."""
492
+ return verify(self.entry, self.stamp)
493
+
494
+ def to_dict(self) -> dict:
495
+ """
496
+ Serialize to a plain dict (for JSON storage).
497
+
498
+ Note on dict structure (PDE F-026):
499
+ The returned dict contains "entry" at the top level alongside
500
+ the stamp fields. The SHA-256 seal was computed WITH entry_text
501
+ as the "entry" field in the seal payload — so the seal does
502
+ cover the entry, but only via the verify() path, not by
503
+ inspecting the dict directly. To confirm a record is intact,
504
+ use SovereignRecord.verify() or module-level verify(entry, stamp).
505
+ Do not attempt to recompute the seal directly from the dict
506
+ fields — use from_dict() then verify().
507
+ """
508
+ d = to_dict(self.stamp)
509
+ d["entry"] = self.entry
510
+ return d
511
+
512
+ @classmethod
513
+ def from_dict(cls, d: dict) -> "SovereignRecord":
514
+ """
515
+ Deserialize a SovereignRecord from a plain dict.
516
+ Validates schema before construction.
517
+ Raises ValueError if required fields are missing or malformed.
518
+
519
+ Note: the dict may contain an "entry" key alongside stamp fields.
520
+ The module-level from_dict() tolerates extra keys (including
521
+ "entry") without raising. This behavior is intentional and
522
+ declared. See PDE F-017.
523
+ """
524
+ if "entry" not in d:
525
+ raise ValueError("from_dict (SovereignRecord): missing field 'entry'")
526
+ entry = d["entry"]
527
+ if not isinstance(entry, str) or not entry:
528
+ raise ValueError("from_dict (SovereignRecord): 'entry' must be a non-empty string")
529
+ stamp_obj = from_dict(d)
530
+ return cls(entry, stamp_obj)
531
+
532
+
533
+ # ═══════════════════════════════════════════════════════════════════
534
+ # VERIFY RESULT — NAMED RETURN
535
+ # ═══════════════════════════════════════════════════════════════════
536
+
537
+ class VerifyResult(NamedTuple):
538
+ """
539
+ Named result from verify(). Evaluates as bool via .valid.
540
+
541
+ valid : bool — True if seal matches entry
542
+ reason : str — one of the values below
543
+
544
+ Reason codes:
545
+ "VALID" — seal matches entry correctly
546
+ "SEAL_MISMATCH" — seal does not match (tampered content, wrong
547
+ entry supplied, or wrong frozen version used
548
+ to verify — see cross-version note in header)
549
+ "TYPE_ERROR" — entry_text is not a str (wrong type, not tampering)
550
+
551
+ IMPORTANT — VERSION_MISMATCH (PDE F-011, F-025, XD-002):
552
+ If you receive SEAL_MISMATCH on a record you trust, check the
553
+ record's "version" field before concluding tampering occurred.
554
+ If version != "FROZEN-4.0", the mismatch is likely because you
555
+ are using the wrong frozen version to verify. Retrieve the
556
+ matching archived version. This distinction will be resolved
557
+ with an explicit "VERSION_MISMATCH" reason code in FROZEN-5.0.
558
+
559
+ VerifyResult(True, "VALID") is truthy.
560
+ VerifyResult(False, "TYPE_ERROR") is falsy — distinct from tampering.
561
+ """
562
+ valid: bool
563
+ reason: str
564
+
565
+ def __bool__(self):
566
+ return self.valid
567
+
568
+
569
+ # ═══════════════════════════════════════════════════════════════════
570
+ # GREGORIAN
571
+ # ═══════════════════════════════════════════════════════════════════
572
+
573
+ _GREGORIAN_MONTHS = (
574
+ "January", "February", "March", "April", "May", "June",
575
+ "July", "August", "September", "October", "November", "December"
576
+ )
577
+
578
+ def _gregorian(d: _date) -> str:
579
+ return f"{_GREGORIAN_MONTHS[d.month - 1]} {d.day}, {d.year}"
580
+
581
+
582
+ # ═══════════════════════════════════════════════════════════════════
583
+ # JULIAN DAY NUMBER (internal bridge — no external use)
584
+ # ═══════════════════════════════════════════════════════════════════
585
+
586
+ _JD_MIN_YEAR = 1582
587
+ _JD_MAX_YEAR = 4000
588
+
589
+ def _jd_from_gregorian(year: int, month: int, day: int) -> int:
590
+ """
591
+ Julian Day Number from Gregorian date.
592
+ Algorithm: Meeus, "Astronomical Algorithms", p. 61.
593
+ Verified: JD(Sep 23, 2025) = 2,460,942
594
+ JD(Mar 3, 2026) = 2,461,103
595
+
596
+ Raises ValueError for pre-reform dates (before Oct 15, 1582)
597
+ and for years outside [1582, 4000].
598
+ """
599
+ if not (_JD_MIN_YEAR <= year <= _JD_MAX_YEAR):
600
+ raise ValueError(
601
+ f"_jd_from_gregorian: year {year} outside valid range "
602
+ f"[{_JD_MIN_YEAR}, {_JD_MAX_YEAR}]. "
603
+ "Dates before Gregorian calendar reform (Oct 15, 1582) "
604
+ "require the Julian calendar formula."
605
+ )
606
+ if year == 1582 and (month < 10 or (month == 10 and day < 15)):
607
+ raise ValueError(
608
+ f"_jd_from_gregorian: date {year}-{month:02d}-{day:02d} is before "
609
+ "Gregorian calendar reform (October 15, 1582). "
610
+ "Pre-reform dates require the Julian calendar formula."
611
+ )
612
+ a = (14 - month) // 12
613
+ y = year + 4800 - a
614
+ m = month + 12 * a - 3
615
+ return (day
616
+ + (153 * m + 2) // 5
617
+ + 365 * y
618
+ + y // 4
619
+ - y // 100
620
+ + y // 400
621
+ - 32045)
622
+
623
+
624
+ # ═══════════════════════════════════════════════════════════════════
625
+ # HEBREW CALENDAR — FULL FOUR DEHIYOT
626
+ # ═══════════════════════════════════════════════════════════════════
627
+
628
+ _HEBREW_EPOCH_JD = 347998
629
+ _LEGAL_YEAR_LENGTHS = frozenset({353, 354, 355, 383, 384, 385})
630
+ _MAX_YEAR_SEARCH = 6 # D&R approximation is within 3 years [S, empirically verified over 1582–4000]
631
+
632
+ _HEBREW_MONTHS_COMMON = (
633
+ "Tishri", "Cheshvan", "Kislev", "Tevet", "Shevat", "Adar",
634
+ "Nisan", "Iyar", "Sivan", "Tammuz","Av", "Elul"
635
+ )
636
+ _HEBREW_MONTHS_LEAP = (
637
+ "Tishri", "Cheshvan", "Kislev", "Tevet", "Shevat", "Adar I", "Adar II",
638
+ "Nisan", "Iyar", "Sivan", "Tammuz","Av", "Elul"
639
+ )
640
+
641
+
642
+ def _is_hebrew_leap(year: int) -> bool:
643
+ """Hebrew leap year: (7y + 1) mod 19 < 7."""
644
+ return (7 * year + 1) % 19 < 7
645
+
646
+
647
+ def _elapsed_days(year: int) -> int:
648
+ """
649
+ Days from Hebrew epoch to 1 Tishri of year — Pass 1.
650
+ Implements Lo ADU Rosh and Molad Zaken.
651
+ D&R §8.1–§8.2.
652
+ """
653
+ months = (235 * year - 234) // 19
654
+ parts = 12084 + 13753 * months
655
+ day = months * 29 + parts // 25920
656
+ if (3 * (day + 1)) % 7 < 3:
657
+ day += 1
658
+ return day
659
+
660
+
661
+ def _new_year_delay(year: int) -> int:
662
+ """
663
+ Additional days to postpone 1 Tishri — Pass 2.
664
+ Handles GaTaRaD and BeTUTeKaPoT. Returns 0, 1, or 2.
665
+ D&R §8.3–§8.4.
666
+
667
+ Mutual exclusion claim (PDE F-008): GaTaRaD (ny2-ny1==356) and
668
+ BeTUTeKaPoT (ny1-ny0==382) cannot be simultaneously true for any
669
+ valid Hebrew year. This is stated as derivable from D&R §8.4 [R].
670
+ The elif ordering follows D&R — GaTaRaD takes priority.
671
+ No runtime assertion is included per FROZEN architecture (no patches);
672
+ this is a FROZEN-5.0 candidate for a debug-mode assertion.
673
+
674
+ Verified: _new_year_delay(5786) = 1 (BeTUTeKaPoT active for 5786)
675
+ """
676
+ ny0 = _elapsed_days(year - 1)
677
+ ny1 = _elapsed_days(year)
678
+ ny2 = _elapsed_days(year + 1)
679
+ if ny2 - ny1 == 356:
680
+ return 2
681
+ elif ny1 - ny0 == 382:
682
+ return 1
683
+ return 0
684
+
685
+
686
+ def _tishri_1_jd(h_year: int) -> int:
687
+ """
688
+ Julian Day of 1 Tishri for the given Hebrew year.
689
+ Full four dehiyot: Pass 1 + Pass 2.
690
+ Civil-day convention.
691
+ Verified: _tishri_1_jd(5786) = 2,460,942 = Sep 23, 2025 [D]
692
+ """
693
+ return _elapsed_days(h_year) + _new_year_delay(h_year) + _HEBREW_EPOCH_JD
694
+
695
+
696
+ def _year_length(h_year: int) -> int:
697
+ return _tishri_1_jd(h_year + 1) - _tishri_1_jd(h_year)
698
+
699
+
700
+ def _month_lengths(h_year: int) -> tuple:
701
+ """
702
+ Ordered month lengths (Tishri → Elul) for h_year.
703
+ Raises ValueError if year length is not in _LEGAL_YEAR_LENGTHS.
704
+ """
705
+ ylen = _year_length(h_year)
706
+ if ylen not in _LEGAL_YEAR_LENGTHS:
707
+ raise ValueError(
708
+ f"Illegal Hebrew year length {ylen} for year {h_year}. "
709
+ f"Legal lengths: {sorted(_LEGAL_YEAR_LENGTHS)}. "
710
+ "Algorithm error — do not patch. Create FROZEN-5.0 with correction."
711
+ )
712
+ leap = _is_hebrew_leap(h_year)
713
+ long_cheshvan = (ylen % 10 == 5)
714
+ short_kislev = (ylen % 10 == 3)
715
+ months = [
716
+ 30,
717
+ 30 if long_cheshvan else 29,
718
+ 29 if short_kislev else 30,
719
+ 29,
720
+ 30,
721
+ 30 if leap else 29,
722
+ ]
723
+ if leap:
724
+ months.append(29)
725
+ months += [30, 29, 30, 29, 30, 29]
726
+ return tuple(months)
727
+
728
+
729
+ def _gregorian_to_hebrew(year: int, month: int, day: int) -> tuple:
730
+ """
731
+ Convert Gregorian date to Hebrew (h_year, month_name, h_day).
732
+ Civil-day convention — aligns with Hebcal and Chabad.org.
733
+ D&R §8.1–§8.4 two-pass.
734
+
735
+ Verified anchors (Hebcal/Chabad):
736
+ gregorian_to_hebrew(2025, 9, 23) == (5786, "Tishri", 1) [D]
737
+ gregorian_to_hebrew(2025, 10, 2) == (5786, "Tishri", 10) [D]
738
+ gregorian_to_hebrew(2026, 3, 3) == (5786, "Adar", 14) [D] [Purim]
739
+ gregorian_to_hebrew(2026, 3, 19) == (5786, "Nisan", 1) [D]
740
+ gregorian_to_hebrew(2026, 4, 2) == (5786, "Nisan", 15) [D] [Passover]
741
+ gregorian_to_hebrew(2024, 3, 25) == (5784, "Adar II", 15) [D] [Leap]
742
+ gregorian_to_hebrew(2025, 9, 22) == (5786, "Elul", 29) [D] [Erev RH]
743
+ gregorian_to_hebrew(2023, 10, 7) == (5784, "Tishri", 22) [D]
744
+ gregorian_to_hebrew(2026, 9, 12) == (5787, "Tishri", 1) [D] [RH 5787]
745
+ gregorian_to_hebrew(2026, 9, 11) == (5786, "Elul", 29) [D] [Erev RH 5787]
746
+
747
+ Raises ValueError on algorithm error or out-of-range input.
748
+ """
749
+ jd = _jd_from_gregorian(year, month, day)
750
+ h_year = (jd - _HEBREW_EPOCH_JD) * 19 // 6935 + 1
751
+
752
+ for _ in range(_MAX_YEAR_SEARCH):
753
+ if _tishri_1_jd(h_year + 1) <= jd:
754
+ h_year += 1
755
+ else:
756
+ break
757
+ else:
758
+ raise ValueError(
759
+ f"Hebrew year (upward) search did not converge for JD {jd} "
760
+ f"({year}-{month}-{day}). Algorithm error — create FROZEN-5.0."
761
+ )
762
+
763
+ for _ in range(_MAX_YEAR_SEARCH):
764
+ if _tishri_1_jd(h_year) > jd:
765
+ h_year -= 1
766
+ else:
767
+ break
768
+ else:
769
+ raise ValueError(
770
+ f"Hebrew year (downward) search did not converge for JD {jd} "
771
+ f"({year}-{month}-{day}). Algorithm error — create FROZEN-5.0."
772
+ )
773
+
774
+ day_of_year = jd - _tishri_1_jd(h_year)
775
+ leap = _is_hebrew_leap(h_year)
776
+ m_lens = _month_lengths(h_year)
777
+ m_names = _HEBREW_MONTHS_LEAP if leap else _HEBREW_MONTHS_COMMON
778
+
779
+ remaining = day_of_year
780
+ for i, length in enumerate(m_lens):
781
+ if remaining < length:
782
+ return h_year, m_names[i], remaining + 1
783
+ remaining -= length
784
+
785
+ raise ValueError(
786
+ f"Hebrew conversion overflowed for {year}-{month}-{day}. "
787
+ "Algorithm error — create FROZEN-5.0."
788
+ )
789
+
790
+
791
+ def _hebrew(d: _date) -> str:
792
+ h_year, h_month, h_day = _gregorian_to_hebrew(d.year, d.month, d.day)
793
+ return f"{h_day} {h_month} {h_year}"
794
+
795
+
796
+ # ═══════════════════════════════════════════════════════════════════
797
+ # 13 MOON DREAMSPELL (Argüelles system)
798
+ # ═══════════════════════════════════════════════════════════════════
799
+ # Disambiguation: This is the José Argüelles 13 Moon calendar.
800
+ # It is NOT the traditional Maya Tzolkin or Haab correlation.
801
+ # ═══════════════════════════════════════════════════════════════════
802
+
803
+ _MOON_NAMES = (
804
+ "Magnetic", "Lunar", "Electric", "Self-Existing",
805
+ "Overtone", "Rhythmic", "Resonant", "Galactic",
806
+ "Solar", "Planetary", "Spectral", "Crystal", "Cosmic"
807
+ )
808
+
809
+ def _dreamspell(d: _date) -> str:
810
+ """
811
+ 13 Moon Dreamspell calendar (Argüelles system).
812
+
813
+ Dead branch note (PDE F-009):
814
+ The delta >= 364 branch below is structurally unreachable.
815
+ In FROZEN-4.0 it returns "Day Out of Time" as a defensive guard.
816
+ FROZEN-5.0 CANDIDATE-01: change this to raise ValueError with a
817
+ diagnostic message rather than returning a plausible-wrong string.
818
+
819
+ Verified anchors:
820
+ dreamspell(date(2026, 3, 3)) == "Day 25, Galactic Moon 8/13" [R]
821
+ dreamspell(date(2026, 7, 25)) == "Day Out of Time" [R]
822
+ dreamspell(date(2026, 7, 26)) == "Day 1, Magnetic Moon 1/13" [R]
823
+ """
824
+ if d.month == 7 and d.day == 25:
825
+ return "Day Out of Time"
826
+
827
+ if (d.month, d.day) >= (7, 26):
828
+ year_start = _date(d.year, 7, 26)
829
+ else:
830
+ year_start = _date(d.year - 1, 7, 26)
831
+
832
+ delta = (d - year_start).days
833
+
834
+ if delta < 0 or delta >= 364: # pragma: no cover — defensive only (FROZEN-5.0 candidate)
835
+ return "Day Out of Time"
836
+
837
+ moon = delta // 28 + 1
838
+ day_num = delta % 28 + 1
839
+ return f"Day {day_num}, {_MOON_NAMES[moon - 1]} Moon {moon}/13"
840
+
841
+
842
+ # ═══════════════════════════════════════════════════════════════════
843
+ # SEAL — SHA-256 CRYPTOGRAPHIC BINDING
844
+ # ═══════════════════════════════════════════════════════════════════
845
+
846
+ _SEAL_PATTERN = re.compile(r'^[0-9a-f]{64}$')
847
+
848
+ # Invisible character categories — used by _validate_entry_text.
849
+ # A trace entry consisting entirely of these code points is invisible
850
+ # and seals nothing meaningful. (Adversarial battery finding — June 2026)
851
+ # Cc=control, Cf=format/invisible, Cs=surrogate, Co=private, Cn=unassigned
852
+ _INVISIBLE_CATEGORIES = frozenset({'Cc', 'Cf', 'Cs', 'Co', 'Cn'})
853
+
854
+
855
+ def _validate_entry_text(entry_text: str) -> str:
856
+ """
857
+ Validate and normalize entry_text. Returns the normalized string.
858
+ Raises TypeError if not a str.
859
+ Raises ValueError if empty after normalization, or if it consists
860
+ entirely of invisible/control Unicode characters.
861
+
862
+ Called by stamp() and stamp_and_record() — single validation point.
863
+
864
+ Invisible character guard:
865
+ str.strip() does not remove zero-width spaces (U+200B), null bytes
866
+ (\x00), or other invisible Unicode code points. An entry consisting
867
+ only of invisible characters seals nothing meaningful and is rejected.
868
+ Test: stamp("\u200b") → ValueError; stamp("\x00") → ValueError.
869
+ """
870
+ if not isinstance(entry_text, str):
871
+ raise TypeError(f"entry_text must be str, got {type(entry_text).__name__}")
872
+ entry_text = unicodedata.normalize("NFC", entry_text).strip()
873
+ if not entry_text:
874
+ raise ValueError("entry_text must be non-empty — an empty trace seals nothing.")
875
+ if all(unicodedata.category(c) in _INVISIBLE_CATEGORIES for c in entry_text):
876
+ raise ValueError(
877
+ "entry_text consists entirely of invisible or control characters — "
878
+ "an invisible trace seals nothing. Provide visible content."
879
+ )
880
+ return entry_text
881
+
882
+
883
+ def _compute_seal(entry_text: str, gregorian: str, hebrew: str,
884
+ dreamspell: str, unix_utc: int) -> str:
885
+ """
886
+ SHA-256 of a deterministic JSON payload.
887
+ Output: 64-character lowercase hex string.
888
+
889
+ Payload field order (sort_keys=True — alphabetical):
890
+ dreamspell, entry, gregorian, hebrew, unix_utc, version
891
+
892
+ INTEGRITY BOUNDARY NOTE:
893
+ This seal proves that entry_text was bound to these exact calendar
894
+ strings at this exact unix_utc by FROZEN-4.0. It does NOT prove
895
+ that the calendar strings are correctly computed. Calendar
896
+ correctness is guaranteed by the self-test and algorithm spec,
897
+ not by the seal itself. See INTEGRITY BOUNDARIES section in the
898
+ module docstring.
899
+
900
+ CLI JSON output uses ensure_ascii=False for human readability.
901
+ This function uses ensure_ascii=True for seal determinism.
902
+ These are different serializations. Never attempt to recompute
903
+ the seal directly from CLI JSON output — use to_dict() → verify().
904
+ (PDE F-010)
905
+ """
906
+ payload = json.dumps(
907
+ {
908
+ "dreamspell": dreamspell,
909
+ "entry": entry_text,
910
+ "gregorian": gregorian,
911
+ "hebrew": hebrew,
912
+ "unix_utc": unix_utc,
913
+ "version": _FROZEN_VERSION,
914
+ },
915
+ sort_keys=True,
916
+ ensure_ascii=True,
917
+ separators=(",", ":"),
918
+ )
919
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
920
+
921
+
922
+ # ═══════════════════════════════════════════════════════════════════
923
+ # PUBLIC INTERFACE
924
+ # ═══════════════════════════════════════════════════════════════════
925
+
926
+ def stamp(entry_text: str, dt: datetime = None) -> SovereignStamp:
927
+ """
928
+ Generate a frozen triple-time cryptographic stamp for an entry.
929
+
930
+ Parameters
931
+ ----------
932
+ entry_text : str
933
+ The sovereign trace entry to seal. Must be non-empty.
934
+ NFC-normalized and stripped before sealing.
935
+ dt : datetime, optional
936
+ Moment to stamp. Defaults to UTC now.
937
+ Must be timezone-aware if provided.
938
+
939
+ Returns
940
+ -------
941
+ SovereignStamp — immutable, triple-time, SHA-256 sealed.
942
+
943
+ Raises
944
+ ------
945
+ ValueError — timezone-naive dt; empty entry; out-of-range date;
946
+ illegal Hebrew year length; Hebrew conversion overflow.
947
+ TypeError — entry_text is not a string.
948
+
949
+ Notes
950
+ -----
951
+ unix_utc is whole seconds only (truncated, not rounded).
952
+ Calendar dates reflect UTC — not local civil date.
953
+
954
+ Temporal consistency: if you call stamp() in a loop without
955
+ supplying dt, entries stamped across a UTC midnight boundary
956
+ will have different calendar dates. Use stamp_batch() or
957
+ supply an explicit dt for temporal consistency. (PDE F-020)
958
+
959
+ Pre-1970 UTC dates: the calendar algorithm supports these but
960
+ unix_utc will be 0 or negative, which will fail from_dict()
961
+ deserialization. See FROZEN-5.0 CANDIDATE-02. (PDE F-006)
962
+ """
963
+ entry_text = _validate_entry_text(entry_text)
964
+
965
+ if dt is None:
966
+ dt = datetime.now(timezone.utc)
967
+ elif dt.tzinfo is None:
968
+ raise ValueError(
969
+ "dt must be timezone-aware. "
970
+ "Use datetime(..., tzinfo=timezone.utc) or datetime.now(timezone.utc)."
971
+ )
972
+
973
+ d = dt.date()
974
+ unix_utc = int(dt.timestamp())
975
+
976
+ g = _gregorian(d)
977
+ h = _hebrew(d)
978
+ ds = _dreamspell(d)
979
+ sl = _compute_seal(entry_text, g, h, ds, unix_utc)
980
+
981
+ return SovereignStamp(
982
+ gregorian = g,
983
+ hebrew = h,
984
+ dreamspell = ds,
985
+ unix_utc = unix_utc,
986
+ seal = sl,
987
+ )
988
+
989
+
990
+ def stamp_and_record(entry_text: str, dt: datetime = None) -> SovereignRecord:
991
+ """
992
+ Generate a SovereignRecord — the canonical {entry, stamp} pair.
993
+ Applies the same NFC normalization as stamp().
994
+ """
995
+ entry_text = _validate_entry_text(entry_text)
996
+ ts = stamp(entry_text, dt)
997
+ return SovereignRecord(entry_text, ts)
998
+
999
+
1000
+ def stamp_batch(entries: list, dt: datetime = None) -> list:
1001
+ """
1002
+ Seal multiple entries at a single canonical timestamp.
1003
+
1004
+ All entries share the same unix_utc, gregorian, hebrew, and
1005
+ dreamspell values. dt is captured once.
1006
+
1007
+ Atomicity: NOT transactional. No rollback. If an entry raises,
1008
+ entries already processed are not rolled back. Validate all
1009
+ entries before calling if all-or-nothing semantics are required.
1010
+ (PDE F-007 — semantic precision note)
1011
+
1012
+ Returns list of SovereignRecord, one per entry, in input order.
1013
+ """
1014
+ if dt is None:
1015
+ dt = datetime.now(timezone.utc)
1016
+ return [stamp_and_record(e, dt) for e in entries]
1017
+
1018
+
1019
+ def display(ts: SovereignStamp) -> str:
1020
+ """Render a SovereignStamp for human display (emoji format, full seal)."""
1021
+ return (
1022
+ f"📅 Gregorian: {ts.gregorian}\n"
1023
+ f"🌑 Hebrew: {ts.hebrew}\n"
1024
+ f"🌀 Dreamspell: {ts.dreamspell}\n"
1025
+ f"🔒 Seal: {ts.seal}\n"
1026
+ f"📌 Version: {ts.version}"
1027
+ )
1028
+
1029
+
1030
+ def display_ascii(ts: SovereignStamp) -> str:
1031
+ """
1032
+ Render a SovereignStamp without emoji (full seal).
1033
+ For logging, piping, ASCII terminals, or CI output.
1034
+ """
1035
+ return (
1036
+ f"Gregorian: {ts.gregorian}\n"
1037
+ f"Hebrew: {ts.hebrew}\n"
1038
+ f"Dreamspell: {ts.dreamspell}\n"
1039
+ f"Seal: {ts.seal}\n"
1040
+ f"Version: {ts.version}"
1041
+ )
1042
+
1043
+
1044
+ def verify(entry_text: str, ts: SovereignStamp) -> VerifyResult:
1045
+ """
1046
+ Verify that entry_text matches the seal in ts.
1047
+
1048
+ Returns VerifyResult — evaluates as bool.
1049
+
1050
+ Reason codes:
1051
+ "VALID" — seal matches entry correctly
1052
+ "TYPE_ERROR" — entry_text is not a str
1053
+ "SEAL_MISMATCH" — seal does not match
1054
+
1055
+ IMPORTANT: SEAL_MISMATCH may mean wrong frozen version used,
1056
+ not necessarily tampered content. Check ts.version. If it is
1057
+ not "FROZEN-4.0", use the matching archived frozen version.
1058
+ A VERSION_MISMATCH reason code will be added in FROZEN-5.0.
1059
+ (PDE F-011, F-025, XD-002 — see FROZEN-5.0 CANDIDATES)
1060
+ """
1061
+ if not isinstance(entry_text, str):
1062
+ return VerifyResult(False, "TYPE_ERROR")
1063
+ entry_text = unicodedata.normalize("NFC", entry_text).strip()
1064
+ expected = _compute_seal(
1065
+ entry_text,
1066
+ ts.gregorian,
1067
+ ts.hebrew,
1068
+ ts.dreamspell,
1069
+ ts.unix_utc,
1070
+ )
1071
+ if expected == ts.seal:
1072
+ return VerifyResult(True, "VALID")
1073
+ return VerifyResult(False, "SEAL_MISMATCH")
1074
+
1075
+
1076
+ def to_dict(ts: SovereignStamp) -> dict:
1077
+ """
1078
+ Serialize SovereignStamp to a plain dict (for JSON storage).
1079
+
1080
+ Note: unix_utc is stored as int. Use int(d["unix_utc"]) when
1081
+ reading back if your JSON layer converts integers to floats.
1082
+ from_dict() enforces int coercion.
1083
+
1084
+ Extra keys in the dict (e.g., "entry" added by SovereignRecord)
1085
+ are tolerated by from_dict() without raising. This behavior is
1086
+ intentional — from_dict() checks for required fields only.
1087
+ (PDE F-017)
1088
+ """
1089
+ return {
1090
+ "gregorian": ts.gregorian,
1091
+ "hebrew": ts.hebrew,
1092
+ "dreamspell": ts.dreamspell,
1093
+ "unix_utc": ts.unix_utc,
1094
+ "seal": ts.seal,
1095
+ "version": ts.version,
1096
+ }
1097
+
1098
+
1099
+ _REQUIRED_FIELDS = frozenset({"gregorian", "hebrew", "dreamspell", "unix_utc", "seal", "version"})
1100
+
1101
+ def from_dict(d: dict) -> SovereignStamp:
1102
+ """
1103
+ Deserialize a SovereignStamp from a plain dict.
1104
+ Validates schema before construction.
1105
+
1106
+ Extra keys in d (beyond _REQUIRED_FIELDS) are tolerated without
1107
+ raising — this is intentional to support SovereignRecord.from_dict()
1108
+ which passes a dict containing an "entry" key alongside stamp fields.
1109
+ (PDE F-017)
1110
+
1111
+ Raises ValueError for missing fields, invalid seal format,
1112
+ non-string calendar fields, version mismatch, or invalid unix_utc.
1113
+ """
1114
+ missing = _REQUIRED_FIELDS - d.keys()
1115
+ if missing:
1116
+ raise ValueError(f"from_dict: missing required fields: {sorted(missing)}")
1117
+
1118
+ if not (isinstance(d["seal"], str) and _SEAL_PATTERN.match(d["seal"])):
1119
+ raise ValueError(
1120
+ f"from_dict: 'seal' must be a 64-character lowercase hex string. "
1121
+ f"Got: {d['seal']!r}"
1122
+ )
1123
+
1124
+ for k in ("gregorian", "hebrew", "dreamspell", "version"):
1125
+ if not (isinstance(d[k], str) and d[k]):
1126
+ raise ValueError(
1127
+ f"from_dict: '{k}' must be a non-empty string. Got: {d[k]!r}"
1128
+ )
1129
+
1130
+ if d["version"] != _FROZEN_VERSION:
1131
+ raise ValueError(
1132
+ f"from_dict: 'version' field is {d['version']!r}. "
1133
+ f"This reader is {_FROZEN_VERSION}. "
1134
+ "Use the matching archived frozen version to deserialize older stamps."
1135
+ )
1136
+
1137
+ try:
1138
+ unix_utc = int(d["unix_utc"])
1139
+ if unix_utc <= 0:
1140
+ raise ValueError("unix_utc must be a positive integer")
1141
+ except (TypeError, ValueError) as exc:
1142
+ raise ValueError(f"from_dict: 'unix_utc' invalid — {exc}") from exc
1143
+
1144
+ return SovereignStamp(
1145
+ gregorian = d["gregorian"],
1146
+ hebrew = d["hebrew"],
1147
+ dreamspell = d["dreamspell"],
1148
+ unix_utc = unix_utc,
1149
+ seal = d["seal"],
1150
+ version = d["version"],
1151
+ )
1152
+
1153
+
1154
+ # ═══════════════════════════════════════════════════════════════════
1155
+ # SELF-TEST — FROZEN VERIFICATION CASES
1156
+ # ═══════════════════════════════════════════════════════════════════
1157
+
1158
+ def _run_self_test(verbose: bool = True) -> None:
1159
+ """
1160
+ Verification against anchor values known at time of writing.
1161
+ Any failure means calendar algorithms have been broken or the
1162
+ file has been modified.
1163
+
1164
+ Do not modify to make tests pass — create FROZEN-5.0.
1165
+
1166
+ Self-test check count: 82 algorithmic checks + 2 sentinel checks
1167
+ + 3 invisible character checks = 87 total.
1168
+ The 2 sentinel checks address PDE F-001 and F-002.
1169
+ The 3 invisible character checks address adversarial battery
1170
+ finding (zero-width space, null byte, invisible char sequence).
1171
+
1172
+ Failure output goes to stderr (PDE F-019, F-024) to prevent
1173
+ corruption of --json mode piped output.
1174
+ """
1175
+ import sys
1176
+ failures = []
1177
+
1178
+ def check(label, actual, expected):
1179
+ if actual != expected:
1180
+ failures.append(f" FAIL — {label}: got {actual!r}, expected {expected!r}")
1181
+ elif verbose:
1182
+ print(f" ✓ {label}: {actual!r}")
1183
+
1184
+ anchor_date = _date(2026, 3, 3)
1185
+ anchor_dt = datetime(2026, 3, 3, 15, 0, 0, tzinfo=timezone.utc)
1186
+
1187
+ if verbose:
1188
+ print("═══ SOVEREIGN TRACE STAMP FROZEN-4.0 — SELF-TEST ═══")
1189
+ print(f"Anchor: {anchor_date} (session of origin)\n")
1190
+
1191
+ # ── SENTINEL STATE CHECK (PDE F-001, F-002) ───────────────────
1192
+ # This check must run first. A pre-release copy with an unset
1193
+ # sentinel should be identified immediately.
1194
+ if _FROZEN_FILE_SHA256 == "SET_AT_RELEASE_TIME":
1195
+ failures.append(
1196
+ " WARN — _FROZEN_FILE_SHA256 is 'SET_AT_RELEASE_TIME'. "
1197
+ "This is a pre-release copy. File integrity cannot be verified. "
1198
+ "Do not deploy until the sentinel is set to the actual file hash."
1199
+ )
1200
+ elif verbose:
1201
+ print(f" ✓ Sentinel is set (not placeholder): {_FROZEN_FILE_SHA256[:16]}...")
1202
+
1203
+ # ── GENESIS SEAL STRUCTURAL CHECK ─────────────────────────────
1204
+ check("Genesis seal is 64-char hex",
1205
+ bool(_SEAL_PATTERN.match(_GENESIS_SEAL)), True)
1206
+ check("Genesis ledger entry declared",
1207
+ _GENESIS_LEDGER_ENTRY, "LEDGER-001-FOUNDING-SEAL")
1208
+
1209
+ # ── Gregorian ─────────────────────────────────────────────────
1210
+ check("Gregorian", _gregorian(anchor_date), "March 3, 2026")
1211
+
1212
+ # ── JD bridge ─────────────────────────────────────────────────
1213
+ check("JD(Sep 23 2025)", _jd_from_gregorian(2025, 9, 23), 2460942)
1214
+ check("JD(Mar 3 2026)", _jd_from_gregorian(2026, 3, 3), 2461103)
1215
+
1216
+ # ── Hebrew epoch ──────────────────────────────────────────────
1217
+ check("1 Tishri 5786 JD", _tishri_1_jd(5786), 2460942)
1218
+ check("1 Tishri 5787 JD", _tishri_1_jd(5787), 2461296)
1219
+
1220
+ # ── Hebrew year structure ─────────────────────────────────────
1221
+ check("5786 is NOT leap", _is_hebrew_leap(5786), False)
1222
+ check("5784 IS leap", _is_hebrew_leap(5784), True)
1223
+ check("5787 IS leap", _is_hebrew_leap(5787), True)
1224
+ check("Year 5786 length", _year_length(5786), 354)
1225
+ check("5786 legal length", _year_length(5786) in _LEGAL_YEAR_LENGTHS, True)
1226
+ check("Year 5785 legal", _year_length(5785) in _LEGAL_YEAR_LENGTHS, True)
1227
+ check("Year 5784 legal", _year_length(5784) in _LEGAL_YEAR_LENGTHS, True)
1228
+
1229
+ # ── Hebrew date conversion ────────────────────────────────────
1230
+ h_year, h_month, h_day = _gregorian_to_hebrew(2026, 3, 3)
1231
+ check("Hebrew year (Mar 3 2026)", h_year, 5786)
1232
+ check("Hebrew month (Mar 3 2026)", h_month, "Adar")
1233
+ check("Hebrew day (Mar 3 2026)", h_day, 14)
1234
+ check("Hebrew string (Mar 3 2026)", _hebrew(anchor_date), "14 Adar 5786")
1235
+
1236
+ # ── Rosh Hashanah ─────────────────────────────────────────────
1237
+ rh_year, rh_month, rh_day = _gregorian_to_hebrew(2025, 9, 23)
1238
+ check("RH 5786 year", rh_year, 5786)
1239
+ check("RH 5786 month", rh_month, "Tishri")
1240
+ check("RH 5786 day", rh_day, 1)
1241
+
1242
+ # ── Erev Rosh Hashanah ────────────────────────────────────────
1243
+ evrh_year, evrh_month, evrh_day = _gregorian_to_hebrew(2025, 9, 22)
1244
+ check("Erev RH 5786 month", evrh_month, "Elul")
1245
+ check("Erev RH 5786 day", evrh_day, 29)
1246
+
1247
+ # ── Yom Kippur ────────────────────────────────────────────────
1248
+ yk_year, yk_month, yk_day = _gregorian_to_hebrew(2025, 10, 2)
1249
+ check("Yom Kippur 5786 month", yk_month, "Tishri")
1250
+ check("Yom Kippur 5786 day", yk_day, 10)
1251
+
1252
+ # ── Passover ──────────────────────────────────────────────────
1253
+ ps_year, ps_month, ps_day = _gregorian_to_hebrew(2026, 4, 2)
1254
+ check("Passover 5786 month", ps_month, "Nisan")
1255
+ check("Passover 5786 day", ps_day, 15)
1256
+
1257
+ # ── 1 Nisan ───────────────────────────────────────────────────
1258
+ n1_year, n1_month, n1_day = _gregorian_to_hebrew(2026, 3, 19)
1259
+ check("1 Nisan 5786 month", n1_month, "Nisan")
1260
+ check("1 Nisan 5786 day", n1_day, 1)
1261
+
1262
+ # ── Leap year anchors ─────────────────────────────────────────
1263
+ leap_y, leap_m, leap_d = _gregorian_to_hebrew(2024, 3, 25)
1264
+ check("Leap year 5784 month (Adar II)", leap_m, "Adar II")
1265
+ check("Leap year 5784 day (15)", leap_d, 15)
1266
+ check("Leap year 5784 year", leap_y, 5784)
1267
+
1268
+ adar1_y, adar1_m, adar1_d = _gregorian_to_hebrew(2024, 2, 10)
1269
+ check("Adar I 5784 month", adar1_m, "Adar I")
1270
+ check("Adar I 5784 day", adar1_d, 1)
1271
+
1272
+ # ── RH 5787 (FROZEN-3.0 CRITICAL fix) ────────────────────────
1273
+ rh87_y, rh87_m, rh87_d = _gregorian_to_hebrew(2026, 9, 12)
1274
+ check("RH 5787 year", rh87_y, 5787)
1275
+ check("RH 5787 month", rh87_m, "Tishri")
1276
+ check("RH 5787 day", rh87_d, 1)
1277
+
1278
+ erh87_y, erh87_m, erh87_d = _gregorian_to_hebrew(2026, 9, 11)
1279
+ check("Erev RH 5787 month", erh87_m, "Elul")
1280
+ check("Erev RH 5787 day", erh87_d, 29)
1281
+
1282
+ sep22_y, sep22_m, sep22_d = _gregorian_to_hebrew(2026, 9, 22)
1283
+ check("Sep 22 2026 day is 11 (NOT 1)", sep22_d, 11)
1284
+ check("Sep 22 2026 month is Tishri", sep22_m, "Tishri")
1285
+
1286
+ # ── Dreamspell ────────────────────────────────────────────────
1287
+ check("Dreamspell Mar 3 2026", _dreamspell(anchor_date),
1288
+ "Day 25, Galactic Moon 8/13")
1289
+ check("Day Out of Time Jul 25", _dreamspell(_date(2026, 7, 25)),
1290
+ "Day Out of Time")
1291
+ check("Dreamspell Jul 26 (Moon 1 Day 1)",
1292
+ _dreamspell(_date(2026, 7, 26)), "Day 1, Magnetic Moon 1/13")
1293
+ check("Dreamspell Mar 1 2028 (post-leap-Feb)",
1294
+ _dreamspell(_date(2028, 3, 1)), "Day 24, Galactic Moon 8/13")
1295
+
1296
+ # ── UTC midnight boundary (PDE F-021) ─────────────────────────
1297
+ # Two stamps one second apart straddling UTC midnight produce
1298
+ # different calendar dates. This is expected behavior.
1299
+ dt_before_midnight = datetime(2026, 3, 3, 23, 59, 59, tzinfo=timezone.utc)
1300
+ dt_after_midnight = datetime(2026, 3, 4, 0, 0, 0, tzinfo=timezone.utc)
1301
+ ts_before = stamp("boundary test before", dt_before_midnight)
1302
+ ts_after = stamp("boundary test after", dt_after_midnight)
1303
+ check("UTC midnight: different dates produce different gregorian",
1304
+ ts_before.gregorian != ts_after.gregorian, True)
1305
+ check("UTC midnight before = March 3", ts_before.gregorian, "March 3, 2026")
1306
+ check("UTC midnight after = March 4", ts_after.gregorian, "March 4, 2026")
1307
+
1308
+ # ── JD range guards ───────────────────────────────────────────
1309
+ try:
1310
+ _jd_from_gregorian(1581, 1, 1)
1311
+ failures.append(" FAIL — Pre-1582 year should raise ValueError")
1312
+ except ValueError:
1313
+ if verbose: print(" ✓ Pre-1582 year correctly rejected")
1314
+
1315
+ try:
1316
+ _jd_from_gregorian(1582, 10, 14)
1317
+ failures.append(" FAIL — 1582-10-14 should raise ValueError")
1318
+ except ValueError:
1319
+ if verbose: print(" ✓ 1582-10-14 pre-reform boundary correctly rejected")
1320
+
1321
+ try:
1322
+ jd_reform = _jd_from_gregorian(1582, 10, 15)
1323
+ if verbose: print(f" ✓ 1582-10-15 accepted: JD {jd_reform}")
1324
+ except ValueError:
1325
+ failures.append(" FAIL — 1582-10-15 should be accepted")
1326
+
1327
+ # ── Unicode normalization ─────────────────────────────────────
1328
+ entry_nfc = "caf\u00e9"
1329
+ entry_nfd = "cafe\u0301"
1330
+ ts_nfc = stamp(entry_nfc, anchor_dt)
1331
+ ts_nfd = stamp(entry_nfd, anchor_dt)
1332
+ check("NFC/NFD normalization: identical seals", ts_nfc.seal, ts_nfd.seal)
1333
+
1334
+ # ── Whitespace normalization ──────────────────────────────────
1335
+ ts_trimmed = stamp("hello", anchor_dt)
1336
+ ts_padded = stamp(" hello ", anchor_dt)
1337
+ check("Whitespace normalization: identical seals", ts_padded.seal, ts_trimmed.seal)
1338
+
1339
+ # ── Version field ─────────────────────────────────────────────
1340
+ ts_anchor = stamp("anchor", anchor_dt)
1341
+ check("Stamp version is FROZEN-4.0", ts_anchor.version, "FROZEN-4.0")
1342
+
1343
+ # ── Seal round-trip ───────────────────────────────────────────
1344
+ test_entry = "Origin trace — Sovereign Trace Protocol FROZEN-4.0."
1345
+ ts = stamp(test_entry, anchor_dt)
1346
+ vr_good = verify(test_entry, ts)
1347
+ vr_alter = verify("altered text", ts)
1348
+ vr_empty = verify("", ts)
1349
+ vr_none = verify(None, ts)
1350
+ check("verify correct entry: valid", vr_good.valid, True)
1351
+ check("verify correct entry: VALID", vr_good.reason, "VALID")
1352
+ check("verify altered: invalid", vr_alter.valid, False)
1353
+ check("verify altered: SEAL_MISMATCH", vr_alter.reason, "SEAL_MISMATCH")
1354
+ check("verify empty: invalid", vr_empty.valid, False)
1355
+ check("verify None: TYPE_ERROR", vr_none.reason, "TYPE_ERROR")
1356
+ check("VerifyResult(True) truthy", bool(vr_good), True)
1357
+ check("VerifyResult(False) falsy", bool(vr_alter), False)
1358
+
1359
+ # ── Hebrew field ──────────────────────────────────────────────
1360
+ check("Stamp Hebrew field", ts.hebrew, "14 Adar 5786")
1361
+
1362
+ # ── check_invariant ───────────────────────────────────────────
1363
+ check("check_invariant on valid stamp", ts.check_invariant(), True)
1364
+
1365
+ # ── __eq__ and __hash__ ───────────────────────────────────────
1366
+ ts_copy = stamp(test_entry, anchor_dt)
1367
+ ts_other = stamp("different entry", anchor_dt)
1368
+ check("Same-input stamps equal", ts == ts_copy, True)
1369
+ check("Same-input stamps share hash", hash(ts) == hash(ts_copy), True)
1370
+ check("Different-entry stamps differ", ts == ts_other, False)
1371
+
1372
+ # ── Immutability ──────────────────────────────────────────────
1373
+ try:
1374
+ ts.gregorian = "tampered"
1375
+ failures.append(" FAIL — SovereignStamp should be immutable")
1376
+ except AttributeError:
1377
+ if verbose: print(" ✓ SovereignStamp immutability enforced")
1378
+
1379
+ # ── SovereignRecord ───────────────────────────────────────────
1380
+ rec = stamp_and_record(test_entry, anchor_dt)
1381
+ check("Record entry matches", rec.entry, test_entry)
1382
+ check("Record verify valid", rec.verify().valid, True)
1383
+ check("Record verify reason", rec.verify().reason, "VALID")
1384
+
1385
+ try:
1386
+ rec.entry = "tampered"
1387
+ failures.append(" FAIL — SovereignRecord should be immutable")
1388
+ except AttributeError:
1389
+ if verbose: print(" ✓ SovereignRecord immutability enforced")
1390
+
1391
+ # ── Serialization round-trip ──────────────────────────────────
1392
+ d2 = to_dict(ts)
1393
+ ts2 = from_dict(d2)
1394
+ check("Round-trip seal", ts2.seal, ts.seal)
1395
+ check("Round-trip version", ts2.version, ts.version)
1396
+ check("Round-trip __eq__", ts2 == ts, True)
1397
+
1398
+ # ── SovereignRecord round-trip ────────────────────────────────
1399
+ rec_dict = rec.to_dict()
1400
+ rec2 = SovereignRecord.from_dict(rec_dict)
1401
+ check("Record round-trip entry", rec2.entry, rec.entry)
1402
+ check("Record round-trip seal", rec2.stamp.seal, rec.stamp.seal)
1403
+ check("Record round-trip verify", rec2.verify().valid, True)
1404
+
1405
+ # ── from_dict validation ──────────────────────────────────────
1406
+ try:
1407
+ from_dict({"gregorian": "x", "hebrew": "y", "dreamspell": "z"})
1408
+ failures.append(" FAIL — from_dict should reject missing fields")
1409
+ except ValueError:
1410
+ if verbose: print(" ✓ from_dict rejects missing fields")
1411
+
1412
+ bad_seal = dict(d2); bad_seal["seal"] = "not-valid"
1413
+ try:
1414
+ from_dict(bad_seal)
1415
+ failures.append(" FAIL — from_dict should reject invalid seal")
1416
+ except ValueError:
1417
+ if verbose: print(" ✓ from_dict rejects invalid seal format")
1418
+
1419
+ bad_ver = dict(d2); bad_ver["version"] = "FROZEN-3.0"
1420
+ try:
1421
+ from_dict(bad_ver)
1422
+ failures.append(" FAIL — from_dict should reject foreign version")
1423
+ except ValueError:
1424
+ if verbose: print(" ✓ from_dict rejects foreign version field")
1425
+
1426
+ # ── stamp_batch ───────────────────────────────────────────────
1427
+ batch = stamp_batch(["first", "second", "third"], anchor_dt)
1428
+ check("Batch length", len(batch), 3)
1429
+ check("Batch shared unix_utc", len({r.stamp.unix_utc for r in batch}), 1)
1430
+ check("Batch record 0 verifies", batch[0].verify().valid, True)
1431
+ check("Batch record 2 verifies", batch[2].verify().valid, True)
1432
+
1433
+ # ── Error handling ────────────────────────────────────────────
1434
+ try:
1435
+ stamp("")
1436
+ failures.append(" FAIL — empty entry should raise ValueError")
1437
+ except ValueError:
1438
+ if verbose: print(" ✓ Empty entry rejected")
1439
+
1440
+ try:
1441
+ stamp(" ")
1442
+ failures.append(" FAIL — whitespace-only entry should raise ValueError")
1443
+ except ValueError:
1444
+ if verbose: print(" ✓ Whitespace-only entry rejected")
1445
+
1446
+ try:
1447
+ stamp("\u200b")
1448
+ failures.append(" FAIL — zero-width space only should raise ValueError")
1449
+ except ValueError:
1450
+ if verbose: print(" ✓ Zero-width space only rejected (invisible char guard)")
1451
+
1452
+ try:
1453
+ stamp("\x00")
1454
+ failures.append(" FAIL — null byte only should raise ValueError")
1455
+ except ValueError:
1456
+ if verbose: print(" ✓ Null byte only rejected (invisible char guard)")
1457
+
1458
+ try:
1459
+ stamp("\u200b\u200c\u200d")
1460
+ failures.append(" FAIL — multiple invisible chars should raise ValueError")
1461
+ except ValueError:
1462
+ if verbose: print(" ✓ Multiple invisible chars rejected (invisible char guard)")
1463
+
1464
+ try:
1465
+ stamp("text", datetime(2026, 3, 3, 15, 0, 0)) # naive
1466
+ failures.append(" FAIL — naive datetime should raise ValueError")
1467
+ except ValueError:
1468
+ if verbose: print(" ✓ Naive datetime rejected")
1469
+
1470
+ # ── FROZEN-1.0 regression ─────────────────────────────────────
1471
+ check("FROZEN-1.0 regression: NOT 15 Adar",
1472
+ ts.hebrew != "15 Adar 5786", True)
1473
+
1474
+ # ── Result (failures to stderr — PDE F-019, F-024) ────────────
1475
+ if verbose:
1476
+ print()
1477
+ if failures:
1478
+ for f in failures:
1479
+ print(f, file=sys.stderr)
1480
+ raise AssertionError(
1481
+ f"{len(failures)} self-test failure(s). "
1482
+ "Do not deploy. Verify algorithms from first principles. "
1483
+ "Create FROZEN-5.0 if correction is needed."
1484
+ )
1485
+ else:
1486
+ if verbose:
1487
+ print("═══ ALL TESTS PASSED ═══")
1488
+ print()
1489
+ print("Anchor stamp (March 3, 2026 — session of origin):")
1490
+ print(display(ts))
1491
+
1492
+
1493
+ # ═══════════════════════════════════════════════════════════════════
1494
+ # CLI — INTERACTIVE TRACE ENTRY
1495
+ # ═══════════════════════════════════════════════════════════════════
1496
+
1497
+ def _cli(json_mode: bool = False) -> None:
1498
+ """
1499
+ Interactive sovereign trace entry with live stamp.
1500
+
1501
+ Privacy notice: entry text submitted via stdin, printed to stdout.
1502
+ On shared systems, stdout may be logged. Shell history may record
1503
+ the invocation. Terminal scrollback may retain the JSON record.
1504
+ Entry text is not encrypted at rest.
1505
+
1506
+ Process memory note: entry text exists in process memory from
1507
+ stdin read until process exit. On multi-tenant systems with shared
1508
+ memory visibility, this is an operator-assessed risk.
1509
+
1510
+ JSON mode note: --json emits only machine-readable JSON to stdout.
1511
+ Self-test failure messages go to stderr to prevent stream corruption.
1512
+ (PDE F-019, F-024)
1513
+
1514
+ Calendar note: calendar dates reflect UTC.
1515
+ Computational permanence note: "This seal is computationally
1516
+ tamper-evident under SHA-256" — not absolutely permanent.
1517
+ See INTEGRITY BOUNDARIES section in module docstring. (MF-M-003)
1518
+ """
1519
+ import sys
1520
+ if not json_mode:
1521
+ print("═══════════════════════════════════════════════════")
1522
+ print(f" SOVEREIGN TRACE PROTOCOL — {_FROZEN_VERSION}")
1523
+ print(" Your trace will be sealed.")
1524
+ print(" The seal is computationally tamper-evident under SHA-256.")
1525
+ print(" Note: calendar dates reflect UTC.")
1526
+ print(" Note: entry will appear in stdout (may be logged).")
1527
+ print("═══════════════════════════════════════════════════\n")
1528
+
1529
+ entry = input("Your trace (present-moment, exact, no narrative arc):\n> ")
1530
+ entry_normalized = unicodedata.normalize("NFC", entry).strip()
1531
+ if not entry_normalized:
1532
+ if not json_mode:
1533
+ print("Empty trace. Nothing sealed.")
1534
+ return
1535
+
1536
+ ts = stamp(entry_normalized)
1537
+ rec = SovereignRecord(entry_normalized, ts)
1538
+
1539
+ if json_mode:
1540
+ print(json.dumps(rec.to_dict(), indent=2, ensure_ascii=False))
1541
+ else:
1542
+ print()
1543
+ print("─── SEALED ───────────────────────────────────────")
1544
+ print(display(ts))
1545
+ print("─── JSON (store this) ────────────────────────────")
1546
+ print(json.dumps(rec.to_dict(), indent=2, ensure_ascii=False))
1547
+ print("──────────────────────────────────────────────────")
1548
+ print("Seal is computationally tamper-evident under SHA-256.")
1549
+ print("Keep the JSON. The ledger append comes next.")
1550
+
1551
+
1552
+ if __name__ == "__main__":
1553
+ import sys
1554
+ if "--test" in sys.argv or "-t" in sys.argv:
1555
+ _run_self_test(verbose=True)
1556
+ else:
1557
+ _run_self_test(verbose=False)
1558
+ _cli(json_mode="--json" in sys.argv)