postmo 1.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.
- postmo/__init__.py +49 -0
- postmo/disciplines.py +138 -0
- postmo/evidence.py +146 -0
- postmo/ledger.py +132 -0
- postmo/overlay.py +74 -0
- postmo/record.py +352 -0
- postmo/scans.py +433 -0
- postmo-1.0.0.dist-info/METADATA +186 -0
- postmo-1.0.0.dist-info/RECORD +12 -0
- postmo-1.0.0.dist-info/WHEEL +5 -0
- postmo-1.0.0.dist-info/licenses/LICENSE +21 -0
- postmo-1.0.0.dist-info/top_level.txt +1 -0
postmo/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""postmo — hold a messy situation without resolving it.
|
|
2
|
+
|
|
3
|
+
Five audit disciplines, enforced as a linter where they're mechanical
|
|
4
|
+
and as explicit operator protocol where they need judgment:
|
|
5
|
+
|
|
6
|
+
1. Hold contradictions as contradictions (parallel sentences, never "X, but Y").
|
|
7
|
+
2. Track people as time-indexed states, never categorical verdicts.
|
|
8
|
+
3. Type every correction: detail vs core-claim.
|
|
9
|
+
4. Never supply motive or causality the teller didn't state.
|
|
10
|
+
5. Self-check every output for softened threat language, supplied
|
|
11
|
+
generous interpretations, dropped contradictions, resolved ambivalence.
|
|
12
|
+
|
|
13
|
+
Two modes: direct audit (the headline — "don't resolve this, just hold
|
|
14
|
+
it", no engine needed) and overlay on the interrogation engine's
|
|
15
|
+
Decompose/Collide phases. Either way the canonical artifact is the
|
|
16
|
+
audit record: full structured data the next session can load, with
|
|
17
|
+
timestamps. The audit never resolves — the record schema rejects
|
|
18
|
+
synthesis/resolution/verdict sections.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .disciplines import DISCIPLINES, D1, D2, D3, D4, D5, discipline_name
|
|
22
|
+
from .evidence import (
|
|
23
|
+
EvidenceError, EvidenceItem, check_matches_engine, normalize_tag, tag_list,
|
|
24
|
+
)
|
|
25
|
+
from .overlay import NEVER, attachment_for, describe_contract
|
|
26
|
+
from .record import (
|
|
27
|
+
PROTOCOL, AuditRecord, DisciplineEntry, RecordError, check_stand_down, parse,
|
|
28
|
+
utc_now,
|
|
29
|
+
)
|
|
30
|
+
from .scans import (
|
|
31
|
+
Correction, CorrectionCandidate, HeldTension, MotiveFlag, PersonState,
|
|
32
|
+
ScanError, ScanReport, SofteningFlag, VerdictWordHit,
|
|
33
|
+
detect_softening, detect_supplied_motive, find_correction_candidates,
|
|
34
|
+
find_subordinators, find_verdict_words, scan_text,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"DISCIPLINES", "D1", "D2", "D3", "D4", "D5", "discipline_name",
|
|
39
|
+
"EvidenceError", "EvidenceItem", "check_matches_engine",
|
|
40
|
+
"normalize_tag", "tag_list",
|
|
41
|
+
"NEVER", "attachment_for", "describe_contract",
|
|
42
|
+
"PROTOCOL", "AuditRecord", "DisciplineEntry", "RecordError",
|
|
43
|
+
"check_stand_down", "parse", "utc_now",
|
|
44
|
+
"Correction", "CorrectionCandidate", "HeldTension", "MotiveFlag",
|
|
45
|
+
"PersonState", "ScanError", "ScanReport", "SofteningFlag",
|
|
46
|
+
"VerdictWordHit", "detect_softening", "detect_supplied_motive",
|
|
47
|
+
"find_correction_candidates", "find_subordinators", "find_verdict_words",
|
|
48
|
+
"scan_text",
|
|
49
|
+
]
|
postmo/disciplines.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The five audit disciplines, stated once.
|
|
2
|
+
|
|
3
|
+
An audit is not a narrative: a narrative resolves tension toward a
|
|
4
|
+
single coherent story; an audit holds the actual shape of what's been
|
|
5
|
+
said — including the parts that don't resolve — and tracks it
|
|
6
|
+
accurately as it changes.
|
|
7
|
+
|
|
8
|
+
Each discipline carries its code/operator split explicitly:
|
|
9
|
+
`mechanical` is what `scans.py` checks; `operator` is what needs a
|
|
10
|
+
human or model judgment and stays protocol, not code. The split is
|
|
11
|
+
honest, not aspirational — a mechanical check that fired is a finding;
|
|
12
|
+
an operator check is a responsibility.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
D1 = 1 # hold contradictions as contradictions
|
|
18
|
+
D2 = 2 # track person-state as time-indexed, not categorical
|
|
19
|
+
D3 = 3 # distinguish detail-correction from core-claim correction
|
|
20
|
+
D4 = 4 # don't supply motive or causality the teller didn't state
|
|
21
|
+
D5 = 5 # self-check for softening and evasion — including your own
|
|
22
|
+
|
|
23
|
+
DISCIPLINES: dict[int, dict[str, str]] = {
|
|
24
|
+
D1: {
|
|
25
|
+
"name": "hold-contradictions",
|
|
26
|
+
"rule": (
|
|
27
|
+
"When two stated things are in tension, do not pick one side, "
|
|
28
|
+
"do not synthesize a third thing that subordinates one to the "
|
|
29
|
+
"other, and do not flag the tension as a problem needing "
|
|
30
|
+
"resolution. Both halves stay live. If a synthesis is true, "
|
|
31
|
+
"the teller states it — never supply it on their behalf."
|
|
32
|
+
),
|
|
33
|
+
"mechanical": (
|
|
34
|
+
"Held tensions must be parallel non-subordinating sentences "
|
|
35
|
+
"('X is true. Y is also true.') — the subordinator scan in "
|
|
36
|
+
"scans.py rejects 'X, but/however/which means Y'."
|
|
37
|
+
),
|
|
38
|
+
"operator": (
|
|
39
|
+
"Recognizing that two statements ARE in tension (the scan "
|
|
40
|
+
"checks grammar, not meaning); refusing to tidy the tension "
|
|
41
|
+
"in surrounding prose; leaving silence where no resolving "
|
|
42
|
+
"sentence is needed."
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
D2: {
|
|
46
|
+
"name": "time-indexed-person-states",
|
|
47
|
+
"rule": (
|
|
48
|
+
"People are not 'good' or 'bad', 'safe' or 'dangerous' as fixed "
|
|
49
|
+
"labels — they are in different states at different points, and "
|
|
50
|
+
"all of those states are real simultaneously. Summarize "
|
|
51
|
+
"sequences of states, never verdicts on persons."
|
|
52
|
+
),
|
|
53
|
+
"mechanical": (
|
|
54
|
+
"PersonState requires when + person + state, all non-empty; "
|
|
55
|
+
"the verdict-word scan flags permanent-label language "
|
|
56
|
+
"('he is a liar') for operator review."
|
|
57
|
+
),
|
|
58
|
+
"operator": (
|
|
59
|
+
"Resisting the pull to declare one state 'the mask' and another "
|
|
60
|
+
"'the truth'; keeping the sequence ordered and complete."
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
D3: {
|
|
64
|
+
"name": "correction-typing",
|
|
65
|
+
"rule": (
|
|
66
|
+
"Detail correction: a particular fact is revised (a time, an "
|
|
67
|
+
"object, a peripheral sequence detail) — it does not touch any "
|
|
68
|
+
"core claim standing elsewhere. Core-claim correction: a "
|
|
69
|
+
"decision, boundary, conclusion, or central assertion is "
|
|
70
|
+
"revised. Never let a detail correction trigger re-litigation "
|
|
71
|
+
"of an uncorrected core claim. If ambiguous which kind, ask "
|
|
72
|
+
"rather than guess."
|
|
73
|
+
),
|
|
74
|
+
"mechanical": (
|
|
75
|
+
"Correction records accept only 'detail' or 'core-claim'; the "
|
|
76
|
+
"correction-candidate scan flags correction-like sentences "
|
|
77
|
+
"('actually…', 'no wait…', 'to be clear…') that haven't been "
|
|
78
|
+
"typed yet."
|
|
79
|
+
),
|
|
80
|
+
"operator": (
|
|
81
|
+
"Typing ambiguous corrections (ask, don't guess); holding the "
|
|
82
|
+
"line that a detail fix does not reopen standing core claims."
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
D4: {
|
|
86
|
+
"name": "no-supplied-motive",
|
|
87
|
+
"rule": (
|
|
88
|
+
"Track what happened and what was said — not why, unless the "
|
|
89
|
+
"teller assigns the why. 'He tackled you because he panicked' "
|
|
90
|
+
"is an invented motive, even if plausible. State the observed "
|
|
91
|
+
"sequence: who did what, in what order. A directly requested "
|
|
92
|
+
"read on motive is a flagged guess ('if I had to guess…'), "
|
|
93
|
+
"never narrated as settled fact."
|
|
94
|
+
),
|
|
95
|
+
"mechanical": (
|
|
96
|
+
"The motive scan flags causal/motive attributions ('because he "
|
|
97
|
+
"panicked', 'driven by', 'in order to') that carry no "
|
|
98
|
+
"attribution marker ('he said', 'according to', quotes) and no "
|
|
99
|
+
"explicit guess-flag."
|
|
100
|
+
),
|
|
101
|
+
"operator": (
|
|
102
|
+
"The final call on whether a flagged attribution was actually "
|
|
103
|
+
"supplied by the teller in surrounding context the scan can't "
|
|
104
|
+
"see; keeping flagged guesses flagged."
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
D5: {
|
|
108
|
+
"name": "softening-self-check",
|
|
109
|
+
"rule": (
|
|
110
|
+
"Before finalizing, check: did a threat/betrayal/harm get "
|
|
111
|
+
"gentler language than the teller used? Did a generous "
|
|
112
|
+
"interpretation get supplied unasked? Did a restatement drop a "
|
|
113
|
+
"contradiction or tidy it? Did ambivalence get resolved into "
|
|
114
|
+
"one 'real' feeling? Did a direct question get answered "
|
|
115
|
+
"evasively? The noticing is part of the deliverable. Also runs "
|
|
116
|
+
"outward: name the teller's own smoothing once, plainly, "
|
|
117
|
+
"without diagnosing — don't press, don't repeat if unpicked."
|
|
118
|
+
),
|
|
119
|
+
"mechanical": (
|
|
120
|
+
"The softening scan compares original vs restatement: "
|
|
121
|
+
"downgrade wordlist ('betrayal' → 'mistake'), dropped strong "
|
|
122
|
+
"terms, tidy-restatement phrases ('so what you're saying is'), "
|
|
123
|
+
"ambivalence-resolution phrases ('the real feeling is')."
|
|
124
|
+
),
|
|
125
|
+
"operator": (
|
|
126
|
+
"Generous-interpretation detection; evasive-answer detection; "
|
|
127
|
+
"judging whether a flagged downgrade is a genuine softening or "
|
|
128
|
+
"the teller's own word choice."
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def discipline_name(n: int) -> str:
|
|
135
|
+
"""Short name for discipline n (1-5)."""
|
|
136
|
+
if n not in DISCIPLINES:
|
|
137
|
+
raise ValueError(f"Unknown discipline: {n!r}. Valid: 1-5.")
|
|
138
|
+
return DISCIPLINES[n]["name"]
|
postmo/evidence.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The evidence taxonomy this package uses.
|
|
2
|
+
|
|
3
|
+
The audit never invented its own evidence tiers — it tracks claims,
|
|
4
|
+
states, and corrections, not tagged evidence. Where the audit record
|
|
5
|
+
carries evidence items, it uses the ONE authoritative taxonomy shared
|
|
6
|
+
across this conversion project:
|
|
7
|
+
|
|
8
|
+
- `adversarial_ifs` (the interrogation engine, `ifs/` conversion):
|
|
9
|
+
11 tags — 10 core tiers plus overlay-only OBFUSCATION.
|
|
10
|
+
- `evasion_audit` (the probe+forge conversion, `probe-forge/`):
|
|
11
|
+
the same 11 tags, vendored with a fork guard.
|
|
12
|
+
|
|
13
|
+
This module vendors the same set a third time, with the same guard:
|
|
14
|
+
`check_matches_engine()` asserts the taxonomies are identical when
|
|
15
|
+
`adversarial_ifs` is importable, and skips when it isn't. Three
|
|
16
|
+
vendored copies plus two guards is clunkier than one shared
|
|
17
|
+
dependency — it is deliberate. Every package in this suite stays
|
|
18
|
+
stdlib-only and installable alone; the guards, not imports, keep the
|
|
19
|
+
taxonomies from silently forking.
|
|
20
|
+
|
|
21
|
+
There is no second taxonomy here. There never was one in the skill.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
# --- The authoritative taxonomy (11 tags) -----------------------------------
|
|
29
|
+
|
|
30
|
+
FACT = "FACT"
|
|
31
|
+
OBSERVATION = "OBSERVATION"
|
|
32
|
+
CLAIM = "CLAIM"
|
|
33
|
+
INFERENCE = "INFERENCE"
|
|
34
|
+
ASSUMPTION = "ASSUMPTION"
|
|
35
|
+
HYPOTHESIS = "HYPOTHESIS"
|
|
36
|
+
REQUIREMENT = "REQUIREMENT"
|
|
37
|
+
CONSTRAINT = "CONSTRAINT"
|
|
38
|
+
DEPENDENCY = "DEPENDENCY"
|
|
39
|
+
UNKNOWN = "UNKNOWN"
|
|
40
|
+
# Overlay-only: set only when the evasion probe ran AND the evasion
|
|
41
|
+
# hypothesis is supported by discriminating evidence.
|
|
42
|
+
OBFUSCATION = "OBFUSCATION"
|
|
43
|
+
|
|
44
|
+
TAXONOMY: dict[str, str] = {
|
|
45
|
+
FACT: "Directly established, unassailable ground truth.",
|
|
46
|
+
OBSERVATION: "Reported or logged, but not independently verified.",
|
|
47
|
+
CLAIM: "An explicit assertion made by an actor or source.",
|
|
48
|
+
INFERENCE: "A conclusion logically drawn from facts or verified observations.",
|
|
49
|
+
ASSUMPTION: "Presumed true without adequate supporting evidence.",
|
|
50
|
+
HYPOTHESIS: "A proposed explanatory model that still needs stress-testing.",
|
|
51
|
+
REQUIREMENT: "A condition that must be true for a model to hold.",
|
|
52
|
+
CONSTRAINT: "A boundary condition limiting viable outcomes.",
|
|
53
|
+
DEPENDENCY: "An element relying directly on the state of another.",
|
|
54
|
+
UNKNOWN: "A critical variable that cannot presently be determined.",
|
|
55
|
+
OBFUSCATION: (
|
|
56
|
+
# Definition text is identical to adversarial_ifs.evidence on
|
|
57
|
+
# purpose — check_matches_engine() enforces byte-identical
|
|
58
|
+
# definitions so the taxonomies can never silently fork.
|
|
59
|
+
"Evasive maneuver or jargon shield masking responsibility. "
|
|
60
|
+
"Tag ONLY when the evasion-probe overlay is active and the evasion "
|
|
61
|
+
"hypothesis is supported by discriminating evidence."
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
CORE_TAGS = frozenset(
|
|
66
|
+
{FACT, OBSERVATION, CLAIM, INFERENCE, ASSUMPTION, HYPOTHESIS,
|
|
67
|
+
REQUIREMENT, CONSTRAINT, DEPENDENCY, UNKNOWN}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EvidenceError(ValueError):
|
|
72
|
+
"""Raised when an evidence item breaks the taxonomy rules."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class EvidenceItem:
|
|
77
|
+
"""One tagged input to the audit."""
|
|
78
|
+
|
|
79
|
+
text: str
|
|
80
|
+
tag: str
|
|
81
|
+
|
|
82
|
+
def __post_init__(self) -> None:
|
|
83
|
+
tag = self.tag.strip().upper()
|
|
84
|
+
if tag not in TAXONOMY:
|
|
85
|
+
raise EvidenceError(
|
|
86
|
+
f"Unknown evidence tag {self.tag!r}. "
|
|
87
|
+
f"Valid tags: {', '.join(sorted(TAXONOMY))}."
|
|
88
|
+
)
|
|
89
|
+
if not self.text.strip():
|
|
90
|
+
raise EvidenceError("Evidence item text must not be empty.")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def normalize_tag(tag: str, *, probe_ran: bool = False) -> str:
|
|
94
|
+
"""Validate and normalize a tag. OBFUSCATION needs the probe.
|
|
95
|
+
|
|
96
|
+
The audit does not run the evasion probe itself. If a record claims
|
|
97
|
+
OBFUSCATION, the caller must pass probe_ran=True — meaning the probe
|
|
98
|
+
ran elsewhere and H-obfuscation was supported by discriminating
|
|
99
|
+
evidence. Suspected-but-untested evasion stays CLAIM.
|
|
100
|
+
"""
|
|
101
|
+
normalized = tag.strip().upper()
|
|
102
|
+
if normalized not in TAXONOMY:
|
|
103
|
+
raise EvidenceError(f"Unknown evidence tag {tag!r}.")
|
|
104
|
+
if normalized == OBFUSCATION and not probe_ran:
|
|
105
|
+
raise EvidenceError(
|
|
106
|
+
"OBFUSCATION is overlay-only: it may be tagged only when the "
|
|
107
|
+
"evasion probe ran and H-obfuscation is supported by "
|
|
108
|
+
"discriminating evidence. Use CLAIM with a hypothesis marker "
|
|
109
|
+
"for suspected-but-untested evasion."
|
|
110
|
+
)
|
|
111
|
+
return normalized
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_matches_engine() -> str:
|
|
115
|
+
"""Assert this taxonomy matches adversarial_ifs' — guard against forks.
|
|
116
|
+
|
|
117
|
+
Returns "matches" on success. Skips (returns "skipped: adversarial_ifs
|
|
118
|
+
not importable") when the sibling package isn't on the path — the
|
|
119
|
+
vendored copy above remains authoritative either way.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
import adversarial_ifs.evidence as engine_evidence
|
|
123
|
+
except ImportError:
|
|
124
|
+
return "skipped: adversarial_ifs not importable"
|
|
125
|
+
ours = set(TAXONOMY)
|
|
126
|
+
theirs = set(engine_evidence.TAXONOMY)
|
|
127
|
+
if ours != theirs:
|
|
128
|
+
raise EvidenceError(
|
|
129
|
+
"Taxonomy fork detected: postmo and adversarial_ifs "
|
|
130
|
+
f"disagree. Only here: {sorted(ours - theirs)}; "
|
|
131
|
+
f"only in engine: {sorted(theirs - ours)}. "
|
|
132
|
+
"Reconcile before shipping."
|
|
133
|
+
)
|
|
134
|
+
for tag in ours:
|
|
135
|
+
if TAXONOMY[tag] != engine_evidence.TAXONOMY[tag]:
|
|
136
|
+
raise EvidenceError(
|
|
137
|
+
f"Taxonomy fork detected: definition of {tag} differs "
|
|
138
|
+
"between postmo and adversarial_ifs. Reconcile "
|
|
139
|
+
"before shipping."
|
|
140
|
+
)
|
|
141
|
+
return "matches"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def tag_list() -> list[str]:
|
|
145
|
+
"""All valid tags, in canonical order."""
|
|
146
|
+
return list(TAXONOMY)
|
postmo/ledger.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Append-only local ledger. Vendored per tool; stdlib only.
|
|
2
|
+
|
|
3
|
+
CANONICAL COPY: edit this file, then re-vendor with vendor.sh.
|
|
4
|
+
Do not edit the vendored copies in place.
|
|
5
|
+
|
|
6
|
+
Privacy rule (non-negotiable): the ledger NEVER stores raw input text.
|
|
7
|
+
Records carry SHA-256 digests of inputs plus structural summaries
|
|
8
|
+
(counts, verdicts, deltas). What happened, and what kind — never the words.
|
|
9
|
+
|
|
10
|
+
Location: ~/.frontface/<tool>/ledger.jsonl
|
|
11
|
+
override: FRONTFACE_LEDGER_DIR=<dir> (ledgers go to <dir>/<tool>/)
|
|
12
|
+
disable: FRONTFACE_NO_LEDGER=1 (record() becomes a silent no-op)
|
|
13
|
+
|
|
14
|
+
The ledger must never break the tool it serves: record() swallows OSError
|
|
15
|
+
and returns False instead of raising.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import fcntl
|
|
28
|
+
except ImportError: # pragma: no cover — Windows has no fcntl
|
|
29
|
+
fcntl = None # type: ignore[assignment]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _base_dir() -> str:
|
|
33
|
+
return os.environ.get("FRONTFACE_LEDGER_DIR") or os.path.join(
|
|
34
|
+
os.path.expanduser("~"), ".frontface"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _disabled() -> bool:
|
|
39
|
+
return os.environ.get("FRONTFACE_NO_LEDGER") == "1"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Ledger:
|
|
43
|
+
"""One JSONL ledger per tool. Cheap to construct; construct per use."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, tool: str) -> None:
|
|
46
|
+
self.tool = tool
|
|
47
|
+
self.dir = os.path.join(_base_dir(), tool)
|
|
48
|
+
self.path = os.path.join(self.dir, "ledger.jsonl")
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def version(self) -> str:
|
|
52
|
+
"""The owning package's __version__, resolved lazily."""
|
|
53
|
+
try:
|
|
54
|
+
pkg = sys.modules.get(__package__ or "")
|
|
55
|
+
return str(getattr(pkg, "__version__", "unknown"))
|
|
56
|
+
except Exception:
|
|
57
|
+
return "unknown"
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def digest(text: str) -> str:
|
|
61
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
62
|
+
|
|
63
|
+
def record(
|
|
64
|
+
self,
|
|
65
|
+
event: str,
|
|
66
|
+
summary: dict,
|
|
67
|
+
*,
|
|
68
|
+
input_text: str | None = None,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""Append one record. Returns False when disabled or on I/O error.
|
|
71
|
+
|
|
72
|
+
Never raises. Never stores raw input text — only its digest.
|
|
73
|
+
"""
|
|
74
|
+
if _disabled():
|
|
75
|
+
return False
|
|
76
|
+
rec: dict = {
|
|
77
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
78
|
+
"tool": self.tool,
|
|
79
|
+
"tool_version": self.version,
|
|
80
|
+
"event": event,
|
|
81
|
+
"summary": summary,
|
|
82
|
+
}
|
|
83
|
+
if input_text is not None:
|
|
84
|
+
rec["input_digest"] = self.digest(input_text)
|
|
85
|
+
try:
|
|
86
|
+
os.makedirs(self.dir, exist_ok=True)
|
|
87
|
+
with open(self.path, "a", encoding="utf-8") as f:
|
|
88
|
+
if fcntl is not None:
|
|
89
|
+
fcntl.flock(f, fcntl.LOCK_EX)
|
|
90
|
+
try:
|
|
91
|
+
f.write(json.dumps(rec) + "\n")
|
|
92
|
+
f.flush()
|
|
93
|
+
os.fsync(f.fileno())
|
|
94
|
+
finally:
|
|
95
|
+
fcntl.flock(f, fcntl.LOCK_UN)
|
|
96
|
+
else:
|
|
97
|
+
f.write(json.dumps(rec) + "\n")
|
|
98
|
+
except OSError:
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def read(self, limit: int | None = None) -> list[dict]:
|
|
103
|
+
"""All records, oldest first. Empty list when there's no ledger."""
|
|
104
|
+
try:
|
|
105
|
+
with open(self.path, encoding="utf-8") as f:
|
|
106
|
+
lines = f.read().splitlines()
|
|
107
|
+
except OSError:
|
|
108
|
+
return []
|
|
109
|
+
recs = []
|
|
110
|
+
for line in lines:
|
|
111
|
+
line = line.strip()
|
|
112
|
+
if line:
|
|
113
|
+
try:
|
|
114
|
+
recs.append(json.loads(line))
|
|
115
|
+
except ValueError:
|
|
116
|
+
continue # never let a corrupt line kill the read
|
|
117
|
+
return recs[-limit:] if limit else recs
|
|
118
|
+
|
|
119
|
+
def summary(self) -> dict:
|
|
120
|
+
"""Counts by event plus first/last timestamps."""
|
|
121
|
+
recs = self.read()
|
|
122
|
+
by_event: dict[str, int] = {}
|
|
123
|
+
for r in recs:
|
|
124
|
+
ev = str(r.get("event", "?"))
|
|
125
|
+
by_event[ev] = by_event.get(ev, 0) + 1
|
|
126
|
+
return {
|
|
127
|
+
"tool": self.tool,
|
|
128
|
+
"records": len(recs),
|
|
129
|
+
"events": by_event,
|
|
130
|
+
"first": recs[0]["ts"] if recs else None,
|
|
131
|
+
"last": recs[-1]["ts"] if recs else None,
|
|
132
|
+
}
|
postmo/overlay.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""The overlay contract: where this audit attaches, and what it never does.
|
|
2
|
+
|
|
3
|
+
As an overlay on the interrogation engine (`adversarial_ifs`), the audit
|
|
4
|
+
attaches at two phase hooks:
|
|
5
|
+
|
|
6
|
+
- Decompose: disciplines 2 (time-indexed person-states), 3 (correction
|
|
7
|
+
typing), and 4 (no supplied motive) govern actor mapping, corrections
|
|
8
|
+
in the source material, and the decomposed field (observed sequences
|
|
9
|
+
only).
|
|
10
|
+
- Collide: discipline 1 (hold contradictions — the strict enforcer: no
|
|
11
|
+
subordinating conjunctions, no synthesized third thing, no flagging
|
|
12
|
+
tensions as problems) and discipline 5 (softening self-check on every
|
|
13
|
+
collision output before it passes onward).
|
|
14
|
+
|
|
15
|
+
The overlay never: resolves a contradiction the engine is holding,
|
|
16
|
+
supplies the synthesis (that's the engine's Synthesize phase, or the
|
|
17
|
+
user's), or converts a direct-mode audit into an interrogation unasked.
|
|
18
|
+
|
|
19
|
+
No engine running: the overlay reports the audit record and stops.
|
|
20
|
+
Direct audit mode is the headline use — "don't resolve this, just hold
|
|
21
|
+
it" — and needs no engine at all.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from .disciplines import D1, D2, D3, D4, D5
|
|
27
|
+
|
|
28
|
+
# Phase hooks: engine phase -> disciplines enforced there.
|
|
29
|
+
ATTACHMENT_POINTS: dict[str, tuple[int, ...]] = {
|
|
30
|
+
"Decompose": (D2, D3, D4),
|
|
31
|
+
"Collide": (D1, D5),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# What the overlay must never do. Enforced at the schema level by
|
|
35
|
+
# record.check_stand_down (no synthesis/resolution/verdict sections)
|
|
36
|
+
# and stated here so the contract is readable in one place.
|
|
37
|
+
NEVER = (
|
|
38
|
+
"resolve a contradiction the engine is holding",
|
|
39
|
+
"supply the synthesis on the teller's behalf",
|
|
40
|
+
"convert a direct-mode audit into an interrogation unasked",
|
|
41
|
+
"soften threat, danger, harm, or betrayal language",
|
|
42
|
+
"treat one correction as license to revisit unrelated standing claims",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OverlayError(ValueError):
|
|
47
|
+
"""Raised when overlay input breaks the contract."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def attachment_for(phase: str) -> tuple[int, ...]:
|
|
51
|
+
"""Disciplines enforced at an engine phase."""
|
|
52
|
+
if phase not in ATTACHMENT_POINTS:
|
|
53
|
+
raise OverlayError(
|
|
54
|
+
f"Unknown engine phase {phase!r}. "
|
|
55
|
+
f"Known: {', '.join(ATTACHMENT_POINTS)}."
|
|
56
|
+
)
|
|
57
|
+
return ATTACHMENT_POINTS[phase]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def describe_contract() -> str:
|
|
61
|
+
"""The contract as human-readable text."""
|
|
62
|
+
lines = ["Audit overlay contract:", ""]
|
|
63
|
+
for phase, disciplines in ATTACHMENT_POINTS.items():
|
|
64
|
+
lines.append(f"- On {phase}: disciplines {', '.join(map(str, disciplines))}.")
|
|
65
|
+
lines.append("")
|
|
66
|
+
lines.append("The overlay never:")
|
|
67
|
+
for item in NEVER:
|
|
68
|
+
lines.append(f"- {item}.")
|
|
69
|
+
lines.append("")
|
|
70
|
+
lines.append(
|
|
71
|
+
"No engine running: report the audit record and stop. "
|
|
72
|
+
"Direct audit mode needs no engine."
|
|
73
|
+
)
|
|
74
|
+
return "\n".join(lines)
|