fairit 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.
- fairit/__init__.py +64 -0
- fairit/brief.py +131 -0
- fairit/dialects.py +54 -0
- fairit/evidence.py +177 -0
- fairit/ground.py +123 -0
- fairit/hypotheses.py +218 -0
- fairit/ledger.py +132 -0
- fairit/overlay.py +184 -0
- fairit-1.0.0.dist-info/METADATA +147 -0
- fairit-1.0.0.dist-info/RECORD +13 -0
- fairit-1.0.0.dist-info/WHEEL +5 -0
- fairit-1.0.0.dist-info/licenses/LICENSE +21 -0
- fairit-1.0.0.dist-info/top_level.txt +1 -0
fairit/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""fairit — pre-interrogation ground mapping and evasion testing.
|
|
2
|
+
|
|
3
|
+
Map the ground (held tensions, time-indexed states, typed corrections),
|
|
4
|
+
test evasion hypotheses (never presume them), and hand off 2-3 seed
|
|
5
|
+
frames to whatever comes next. The package never collides, adjudicates,
|
|
6
|
+
refines, or extracts surprise — it audits and hands off.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .brief import AuditBrief, BriefError, check_stand_down, parse
|
|
10
|
+
from .dialects import DIALECTS, SHIELD_MARKERS, detect_markers
|
|
11
|
+
from .evidence import (
|
|
12
|
+
TAXONOMY,
|
|
13
|
+
EvidenceError,
|
|
14
|
+
EvidenceItem,
|
|
15
|
+
check_matches_engine,
|
|
16
|
+
forge_mapping_table,
|
|
17
|
+
normalize_tag,
|
|
18
|
+
tag_list,
|
|
19
|
+
)
|
|
20
|
+
from .ground import (
|
|
21
|
+
Correction,
|
|
22
|
+
GroundError,
|
|
23
|
+
GroundMap,
|
|
24
|
+
HeldTension,
|
|
25
|
+
PersonState,
|
|
26
|
+
find_subordinators,
|
|
27
|
+
)
|
|
28
|
+
from .hypotheses import (
|
|
29
|
+
H_BOTH,
|
|
30
|
+
H_GENUINE,
|
|
31
|
+
H_OBFUSCATION,
|
|
32
|
+
INSUFFICIENT_EVIDENCE,
|
|
33
|
+
SUPPORT_STATUSES,
|
|
34
|
+
VERDICTS,
|
|
35
|
+
CategoryPresumptionError,
|
|
36
|
+
DiscriminatingEvidence,
|
|
37
|
+
HypothesisError,
|
|
38
|
+
HypothesisRecord,
|
|
39
|
+
Mechanism,
|
|
40
|
+
)
|
|
41
|
+
from .overlay import (
|
|
42
|
+
JargonFlag,
|
|
43
|
+
ObfuscatedObject,
|
|
44
|
+
ProbeError,
|
|
45
|
+
evidence_tag_for,
|
|
46
|
+
run_jargon_test,
|
|
47
|
+
scan_and_test,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"AuditBrief", "BriefError", "check_stand_down", "parse",
|
|
52
|
+
"DIALECTS", "SHIELD_MARKERS", "detect_markers",
|
|
53
|
+
"TAXONOMY", "EvidenceError", "EvidenceItem", "check_matches_engine",
|
|
54
|
+
"forge_mapping_table", "normalize_tag", "tag_list",
|
|
55
|
+
"Correction", "GroundError", "GroundMap", "HeldTension", "PersonState",
|
|
56
|
+
"find_subordinators",
|
|
57
|
+
"H_BOTH", "H_GENUINE", "H_OBFUSCATION", "INSUFFICIENT_EVIDENCE",
|
|
58
|
+
"SUPPORT_STATUSES", "VERDICTS", "CategoryPresumptionError",
|
|
59
|
+
"DiscriminatingEvidence", "HypothesisError", "HypothesisRecord", "Mechanism",
|
|
60
|
+
"JargonFlag", "ObfuscatedObject", "ProbeError", "evidence_tag_for",
|
|
61
|
+
"run_jargon_test", "scan_and_test",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
__version__ = "1.0.0"
|
fairit/brief.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""The audit brief — the overlay's output artifact.
|
|
2
|
+
|
|
3
|
+
The brief records what was audited, which evasion hypotheses were tested
|
|
4
|
+
and their support status, and what seeds were handed off. It is a brief,
|
|
5
|
+
not an interrogation result: the interrogation engine's state artifact
|
|
6
|
+
stays canonical.
|
|
7
|
+
|
|
8
|
+
Stand-down rule, enforced in code: the brief must NEVER contain
|
|
9
|
+
collision, adjudication, refinement, or surprise sections. Those belong
|
|
10
|
+
to the interrogation engine. If the engine isn't running, the brief is
|
|
11
|
+
reported and the work stops.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import date
|
|
19
|
+
|
|
20
|
+
from .ground import GroundMap
|
|
21
|
+
from .hypotheses import HypothesisRecord, H_OBFUSCATION
|
|
22
|
+
|
|
23
|
+
# Headings the overlay must never emit. A brief that collides or
|
|
24
|
+
# adjudicates has left its lane — the schema rejects it.
|
|
25
|
+
FORBIDDEN_SECTIONS = (
|
|
26
|
+
"collision", "collide", "adjudicat", "refinement", "refine",
|
|
27
|
+
"surprise",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BriefError(ValueError):
|
|
32
|
+
"""Raised when a brief breaks the schema or the stand-down rule."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_stand_down(rendered: str) -> None:
|
|
36
|
+
"""Reject rendered text that contains engine-owned sections."""
|
|
37
|
+
headings = re.findall(r"^#{1,3}\s*(.+)$", rendered, re.MULTILINE)
|
|
38
|
+
for heading in headings:
|
|
39
|
+
lowered = heading.lower()
|
|
40
|
+
for forbidden in FORBIDDEN_SECTIONS:
|
|
41
|
+
if forbidden in lowered:
|
|
42
|
+
raise BriefError(
|
|
43
|
+
f"Stand-down violation: the brief must not contain a "
|
|
44
|
+
f"'{heading.strip()}' section. Collision, adjudication, "
|
|
45
|
+
"refinement, and surprise belong to the interrogation "
|
|
46
|
+
"engine — this overlay audits and hands off, nothing more."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class AuditBrief:
|
|
52
|
+
"""Audited ground + tested hypotheses + handoff seeds."""
|
|
53
|
+
|
|
54
|
+
case_name: str
|
|
55
|
+
ground: GroundMap = field(default_factory=GroundMap)
|
|
56
|
+
records: list[HypothesisRecord] = field(default_factory=list)
|
|
57
|
+
seeds: list[str] = field(default_factory=list)
|
|
58
|
+
lifecycle: str = "FINAL"
|
|
59
|
+
brief_date: str = field(default_factory=lambda: date.today().isoformat())
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if not self.case_name.strip():
|
|
63
|
+
raise BriefError("Brief needs a case name.")
|
|
64
|
+
if self.lifecycle not in ("INITIAL", "ITERATIVE", "FINAL"):
|
|
65
|
+
raise BriefError(f"Bad lifecycle: {self.lifecycle!r}.")
|
|
66
|
+
if not (2 <= len(self.seeds) <= 3):
|
|
67
|
+
raise BriefError(
|
|
68
|
+
f"Handoff must produce 2-3 seed frames, got {len(self.seeds)}."
|
|
69
|
+
)
|
|
70
|
+
for seed in self.seeds:
|
|
71
|
+
if not seed.strip():
|
|
72
|
+
raise BriefError("Seed frames must not be empty.")
|
|
73
|
+
|
|
74
|
+
def obfuscation_supported(self) -> bool:
|
|
75
|
+
"""True only when some record's H-obfuscation is supported."""
|
|
76
|
+
return any(r.supports_obfuscation() for r in self.records)
|
|
77
|
+
|
|
78
|
+
def render(self) -> str:
|
|
79
|
+
lines = [
|
|
80
|
+
"---",
|
|
81
|
+
"artifact: audit-brief",
|
|
82
|
+
f"date: {self.brief_date}",
|
|
83
|
+
"protocol: fairit/1.0",
|
|
84
|
+
f"lifecycle: {self.lifecycle}",
|
|
85
|
+
"---",
|
|
86
|
+
"",
|
|
87
|
+
f"# Audit brief: {self.case_name}",
|
|
88
|
+
"",
|
|
89
|
+
self.ground.render(),
|
|
90
|
+
"",
|
|
91
|
+
"## Tested hypotheses",
|
|
92
|
+
]
|
|
93
|
+
for record in self.records:
|
|
94
|
+
lines.append("")
|
|
95
|
+
lines.append(record.summary())
|
|
96
|
+
lines.extend(["", "## Handoff seeds"])
|
|
97
|
+
for i, seed in enumerate(self.seeds, 1):
|
|
98
|
+
lines.append(f"{i}. {seed}")
|
|
99
|
+
rendered = "\n".join(lines)
|
|
100
|
+
check_stand_down(rendered)
|
|
101
|
+
return rendered
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse(rendered: str) -> AuditBrief:
|
|
105
|
+
"""Parse a rendered brief back into an AuditBrief.
|
|
106
|
+
|
|
107
|
+
Round-trip fidelity: case name, lifecycle, date, and seeds survive.
|
|
108
|
+
Ground and hypothesis records parse back as their raw text — the
|
|
109
|
+
structured re-validation is the caller's job on re-entry.
|
|
110
|
+
"""
|
|
111
|
+
check_stand_down(rendered)
|
|
112
|
+
front = dict(
|
|
113
|
+
line.split(":", 1)
|
|
114
|
+
for line in rendered.splitlines()
|
|
115
|
+
if re.match(r"^(artifact|date|protocol|lifecycle):", line)
|
|
116
|
+
)
|
|
117
|
+
if front.get("artifact", "").strip() != "audit-brief":
|
|
118
|
+
raise BriefError("Not an audit-brief artifact.")
|
|
119
|
+
match = re.search(r"^# Audit brief: (.+)$", rendered, re.MULTILINE)
|
|
120
|
+
if not match:
|
|
121
|
+
raise BriefError("Brief is missing its case-name heading.")
|
|
122
|
+
seed_lines: list[str] = []
|
|
123
|
+
if "## Handoff seeds" in rendered:
|
|
124
|
+
tail = rendered.split("## Handoff seeds", 1)[1]
|
|
125
|
+
seed_lines = [s.strip() for s in re.findall(r"^\d+\. (.+)$", tail, re.MULTILINE)]
|
|
126
|
+
return AuditBrief(
|
|
127
|
+
case_name=match.group(1).strip(),
|
|
128
|
+
lifecycle=front.get("lifecycle", "FINAL").strip() or "FINAL",
|
|
129
|
+
brief_date=front.get("date", "").strip() or date.today().isoformat(),
|
|
130
|
+
seeds=seed_lines,
|
|
131
|
+
)
|
fairit/dialects.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shield-vocabulary marker lists, by dialect.
|
|
2
|
+
|
|
3
|
+
These lists detect *candidate* instances of jargon that MAY function as
|
|
4
|
+
evasion. Detection is the start of the hypothesis protocol, never its
|
|
5
|
+
conclusion: a marker hit feeds H-obfuscation / H-genuine / H-both
|
|
6
|
+
testing in `hypotheses.py`. No marker, list, or dialect ever produces a
|
|
7
|
+
verdict by itself.
|
|
8
|
+
|
|
9
|
+
The original skill only knew therapy-speak. The same structure covers
|
|
10
|
+
corporate-speak and bureaucratic passive voice — the mechanism doesn't
|
|
11
|
+
care which dialect the shield is written in.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
# (dialect, phrases). Phrases are matched case-insensitively as
|
|
17
|
+
# substrings — they are tripwires, not judgments.
|
|
18
|
+
SHIELD_MARKERS: dict[str, tuple[str, ...]] = {
|
|
19
|
+
"therapy": (
|
|
20
|
+
"holding space", "doing the work", "inner child", "boundaries",
|
|
21
|
+
"triggered", "processing", "my truth", "self-care", "nervous system",
|
|
22
|
+
"regulation", "co-regulation", "shadow work", "attachment style",
|
|
23
|
+
"parts of me", "sitting with", "unpacking",
|
|
24
|
+
),
|
|
25
|
+
"corporate": (
|
|
26
|
+
"circle back", "synergy", "leverage", "bandwidth", "deep dive",
|
|
27
|
+
"move the needle", "take this offline", "low-hanging fruit",
|
|
28
|
+
"best practices", "thought leadership", "rightsizing",
|
|
29
|
+
"synergize", "actionable", "paradigm",
|
|
30
|
+
),
|
|
31
|
+
"bureaucratic": (
|
|
32
|
+
"mistakes were made", "regrettable", "inappropriate", "it was decided",
|
|
33
|
+
"processes were followed", "at this time", "going forward",
|
|
34
|
+
"lessons learned", "unfortunate", "miscommunication",
|
|
35
|
+
"administrative error", "procedural",
|
|
36
|
+
),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
DIALECTS = tuple(SHIELD_MARKERS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_markers(text: str) -> list[tuple[str, str]]:
|
|
43
|
+
"""Find candidate shield-vocabulary instances.
|
|
44
|
+
|
|
45
|
+
Returns a list of (dialect, phrase) hits. A hit is a candidate for
|
|
46
|
+
hypothesis testing — it says "examine this," nothing more.
|
|
47
|
+
"""
|
|
48
|
+
lowered = text.lower()
|
|
49
|
+
hits: list[tuple[str, str]] = []
|
|
50
|
+
for dialect, phrases in SHIELD_MARKERS.items():
|
|
51
|
+
for phrase in phrases:
|
|
52
|
+
if phrase in lowered:
|
|
53
|
+
hits.append((dialect, phrase))
|
|
54
|
+
return hits
|
fairit/evidence.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""The authoritative evidence taxonomy for this package.
|
|
2
|
+
|
|
3
|
+
Lineage: the interrogation engine (`adversarial_ifs`, in the sibling
|
|
4
|
+
`ifs/` conversion) defines an 11-tag taxonomy — 10 core tiers plus one
|
|
5
|
+
overlay-only tag (OBFUSCATION). field-forge shipped its own 8-tier list.
|
|
6
|
+
This module is the ONE authoritative mapping: field-forge's 8 tiers map
|
|
7
|
+
1:1 onto the engine's taxonomy, and the 3 tiers the forge never named
|
|
8
|
+
(HYPOTHESIS, REQUIREMENT, DEPENDENCY) are documented as recognized —
|
|
9
|
+
not as a second taxonomy.
|
|
10
|
+
|
|
11
|
+
The authoritative set is vendored here (with the lineage note above) so
|
|
12
|
+
this package stays standalone, stdlib-only, with no path dependency on
|
|
13
|
+
its sibling. `check_matches_engine()` guards against silent forks: if
|
|
14
|
+
`adversarial_ifs` is importable, it asserts the two taxonomies are
|
|
15
|
+
identical; if it isn't, the check skips and the vendored copy stands.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
# --- The authoritative taxonomy (11 tags) -----------------------------------
|
|
23
|
+
|
|
24
|
+
FACT = "FACT"
|
|
25
|
+
OBSERVATION = "OBSERVATION"
|
|
26
|
+
CLAIM = "CLAIM"
|
|
27
|
+
INFERENCE = "INFERENCE"
|
|
28
|
+
ASSUMPTION = "ASSUMPTION"
|
|
29
|
+
HYPOTHESIS = "HYPOTHESIS"
|
|
30
|
+
REQUIREMENT = "REQUIREMENT"
|
|
31
|
+
CONSTRAINT = "CONSTRAINT"
|
|
32
|
+
DEPENDENCY = "DEPENDENCY"
|
|
33
|
+
UNKNOWN = "UNKNOWN"
|
|
34
|
+
# Overlay-only: set only when the evasion probe ran AND the evasion
|
|
35
|
+
# hypothesis is supported by discriminating evidence.
|
|
36
|
+
OBFUSCATION = "OBFUSCATION"
|
|
37
|
+
|
|
38
|
+
TAXONOMY: dict[str, str] = {
|
|
39
|
+
FACT: "Directly established, unassailable ground truth.",
|
|
40
|
+
OBSERVATION: "Reported or logged, but not independently verified.",
|
|
41
|
+
CLAIM: "An explicit assertion made by an actor or source.",
|
|
42
|
+
INFERENCE: "A conclusion logically drawn from facts or verified observations.",
|
|
43
|
+
ASSUMPTION: "Presumed true without adequate supporting evidence.",
|
|
44
|
+
HYPOTHESIS: "A proposed explanatory model that still needs stress-testing.",
|
|
45
|
+
REQUIREMENT: "A condition that must be true for a model to hold.",
|
|
46
|
+
CONSTRAINT: "A boundary condition limiting viable outcomes.",
|
|
47
|
+
DEPENDENCY: "An element relying directly on the state of another.",
|
|
48
|
+
UNKNOWN: "A critical variable that cannot presently be determined.",
|
|
49
|
+
OBFUSCATION: (
|
|
50
|
+
# Definition text is identical to adversarial_ifs.evidence on
|
|
51
|
+
# purpose — check_matches_engine() enforces byte-identical
|
|
52
|
+
# definitions so the two taxonomies can never silently fork.
|
|
53
|
+
"Evasive maneuver or jargon shield masking responsibility. "
|
|
54
|
+
"Tag ONLY when the evasion-probe overlay is active and the evasion "
|
|
55
|
+
"hypothesis is supported by discriminating evidence."
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CORE_TAGS = frozenset(
|
|
60
|
+
{FACT, OBSERVATION, CLAIM, INFERENCE, ASSUMPTION, HYPOTHESIS,
|
|
61
|
+
REQUIREMENT, CONSTRAINT, DEPENDENCY, UNKNOWN}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EvidenceError(ValueError):
|
|
66
|
+
"""Raised when an evidence item breaks the taxonomy rules."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class EvidenceItem:
|
|
71
|
+
"""One tagged input to the audit."""
|
|
72
|
+
|
|
73
|
+
text: str
|
|
74
|
+
tag: str
|
|
75
|
+
|
|
76
|
+
def __post_init__(self) -> None:
|
|
77
|
+
tag = self.tag.strip().upper()
|
|
78
|
+
if tag not in TAXONOMY:
|
|
79
|
+
raise EvidenceError(
|
|
80
|
+
f"Unknown evidence tag {self.tag!r}. "
|
|
81
|
+
f"Valid tags: {', '.join(sorted(TAXONOMY))}."
|
|
82
|
+
)
|
|
83
|
+
if not self.text.strip():
|
|
84
|
+
raise EvidenceError("Evidence item text must not be empty.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def normalize_tag(tag: str, *, probe_ran: bool = False) -> str:
|
|
88
|
+
"""Validate and normalize a tag. OBFUSCATION needs the probe.
|
|
89
|
+
|
|
90
|
+
The probe-run requirement is the taxonomy-level twin of the
|
|
91
|
+
hypothesis gate in `hypotheses.py`: the tag may exist on paper,
|
|
92
|
+
but it may only be *used* when H-obfuscation was actually supported.
|
|
93
|
+
"""
|
|
94
|
+
normalized = tag.strip().upper()
|
|
95
|
+
if normalized not in TAXONOMY:
|
|
96
|
+
raise EvidenceError(f"Unknown evidence tag {tag!r}.")
|
|
97
|
+
if normalized == OBFUSCATION and not probe_ran:
|
|
98
|
+
raise EvidenceError(
|
|
99
|
+
"OBFUSCATION is overlay-only: it may be tagged only when the "
|
|
100
|
+
"evasion probe ran and H-obfuscation is supported by "
|
|
101
|
+
"discriminating evidence. Use CLAIM with a hypothesis marker "
|
|
102
|
+
"for suspected-but-untested evasion."
|
|
103
|
+
)
|
|
104
|
+
return normalized
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- The field-forge reconciliation ------------------------------------------
|
|
108
|
+
#
|
|
109
|
+
# field-forge's 8-tier list (from its SKILL.md), mapped onto the
|
|
110
|
+
# authoritative taxonomy. Every forge tier maps 1:1 — no renames, no
|
|
111
|
+
# splits. The 3 tiers the forge never named are listed as RECOGNIZED:
|
|
112
|
+
# valid tags the forge simply didn't use, now available everywhere.
|
|
113
|
+
#
|
|
114
|
+
# forge FACT -> FACT
|
|
115
|
+
# forge OBSERVATION -> OBSERVATION
|
|
116
|
+
# forge CLAIM -> CLAIM
|
|
117
|
+
# forge OBFUSCATION -> OBFUSCATION (same overlay-only rule)
|
|
118
|
+
# forge INFERENCE -> INFERENCE
|
|
119
|
+
# forge ASSUMPTION -> ASSUMPTION
|
|
120
|
+
# forge CONSTRAINT -> CONSTRAINT
|
|
121
|
+
# forge UNKNOWN -> UNKNOWN
|
|
122
|
+
# (not named by forge) HYPOTHESIS, REQUIREMENT, DEPENDENCY -> RECOGNIZED
|
|
123
|
+
|
|
124
|
+
FORGE_TIERS = frozenset(
|
|
125
|
+
{"FACT", "OBSERVATION", "CLAIM", "OBFUSCATION", "INFERENCE",
|
|
126
|
+
"ASSUMPTION", "CONSTRAINT", "UNKNOWN"}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
FORGE_MISSING = frozenset({"HYPOTHESIS", "REQUIREMENT", "DEPENDENCY"})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def forge_mapping_table() -> str:
|
|
133
|
+
"""The reconciliation, as a human-readable table."""
|
|
134
|
+
lines = [
|
|
135
|
+
"| field-forge 8-tier | authoritative tag | status |",
|
|
136
|
+
"|--------------------|-------------------|--------|",
|
|
137
|
+
]
|
|
138
|
+
for tier in sorted(FORGE_TIERS):
|
|
139
|
+
lines.append(f"| {tier} | {tier} | 1:1 map |")
|
|
140
|
+
for tier in sorted(FORGE_MISSING):
|
|
141
|
+
lines.append(f"| (not named) | {tier} | recognized |")
|
|
142
|
+
return "\n".join(lines)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check_matches_engine() -> str:
|
|
146
|
+
"""Assert this taxonomy matches adversarial_ifs' — guard against forks.
|
|
147
|
+
|
|
148
|
+
Returns "matches" on success. Skips (returns "skipped: adversarial_ifs
|
|
149
|
+
not importable") when the sibling package isn't on the path — the
|
|
150
|
+
vendored copy above remains authoritative either way.
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
import adversarial_ifs.evidence as engine_evidence
|
|
154
|
+
except ImportError:
|
|
155
|
+
return "skipped: adversarial_ifs not importable"
|
|
156
|
+
ours = set(TAXONOMY)
|
|
157
|
+
theirs = set(engine_evidence.TAXONOMY)
|
|
158
|
+
if ours != theirs:
|
|
159
|
+
raise EvidenceError(
|
|
160
|
+
"Taxonomy fork detected: fairit and adversarial_ifs "
|
|
161
|
+
f"disagree. Only here: {sorted(ours - theirs)}; "
|
|
162
|
+
f"only in engine: {sorted(theirs - ours)}. "
|
|
163
|
+
"Reconcile before shipping."
|
|
164
|
+
)
|
|
165
|
+
for tag in ours:
|
|
166
|
+
if TAXONOMY[tag] != engine_evidence.TAXONOMY[tag]:
|
|
167
|
+
raise EvidenceError(
|
|
168
|
+
f"Taxonomy fork detected: definition of {tag} differs "
|
|
169
|
+
"between fairit and adversarial_ifs. Reconcile "
|
|
170
|
+
"before shipping."
|
|
171
|
+
)
|
|
172
|
+
return "matches"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def tag_list() -> list[str]:
|
|
176
|
+
"""All valid tags, in canonical order."""
|
|
177
|
+
return list(TAXONOMY)
|
fairit/ground.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Ground mapping — the pre-interrogation audit of situational state.
|
|
2
|
+
|
|
3
|
+
Objective: establish exactly what is the case WITHOUT forcing narrative
|
|
4
|
+
resolution. Three outputs:
|
|
5
|
+
|
|
6
|
+
- Held tensions: opposing facts stated as parallel, non-subordinating
|
|
7
|
+
sentences ("X is true. Y is also true."). Never "X, but Y" — the
|
|
8
|
+
subordinating conjunction quietly privileges one side, which is a
|
|
9
|
+
resolution smuggled in as grammar.
|
|
10
|
+
- Time-indexed person-state sequence: people change across time; map the
|
|
11
|
+
states, never flatten a person to one characterization.
|
|
12
|
+
- Corrections typed as detail vs core-claim: fixing a date is a detail;
|
|
13
|
+
changing what was claimed to have happened is a core-claim change —
|
|
14
|
+
and the latter is the signal.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
# Subordinating conjunctions that privilege one side of a tension.
|
|
23
|
+
# "X is true. Y is also true." stays. "X is true, but Y" is rejected —
|
|
24
|
+
# the "but" resolves the contradiction by demoting X.
|
|
25
|
+
SUBORDINATORS = (
|
|
26
|
+
"but", "however", "although", "though", "yet", "whereas",
|
|
27
|
+
"nevertheless", "nonetheless", "which means", "in other words",
|
|
28
|
+
"so really", "that is to say",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_SUB_RE = re.compile(
|
|
32
|
+
r"\b(" + "|".join(re.escape(s) for s in SUBORDINATORS) + r")\b",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GroundError(ValueError):
|
|
38
|
+
"""Raised when mapped ground breaks the audit rules."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_subordinators(text: str) -> list[str]:
|
|
42
|
+
"""Return the subordinating conjunctions found in the text."""
|
|
43
|
+
return sorted({m.group(0).lower() for m in _SUB_RE.finditer(text)})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class HeldTension:
|
|
48
|
+
"""One pair of opposing facts, held live as parallel sentences."""
|
|
49
|
+
|
|
50
|
+
text: str
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
if not self.text.strip():
|
|
54
|
+
raise GroundError("Held tension must not be empty.")
|
|
55
|
+
bad = find_subordinators(self.text)
|
|
56
|
+
if bad:
|
|
57
|
+
raise GroundError(
|
|
58
|
+
f"Held tension uses subordinating language {bad}: "
|
|
59
|
+
f"{self.text!r}. Restate as parallel non-subordinating "
|
|
60
|
+
"sentences — 'X is true. Y is also true.'"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class PersonState:
|
|
66
|
+
"""One person, one moment, one observed state. Time-indexed."""
|
|
67
|
+
|
|
68
|
+
when: str
|
|
69
|
+
person: str
|
|
70
|
+
state: str
|
|
71
|
+
|
|
72
|
+
def __post_init__(self) -> None:
|
|
73
|
+
for label, value in (("when", self.when), ("person", self.person),
|
|
74
|
+
("state", self.state)):
|
|
75
|
+
if not value.strip():
|
|
76
|
+
raise GroundError(f"PersonState.{label} must not be empty.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class Correction:
|
|
81
|
+
"""A correction typed by what changed.
|
|
82
|
+
|
|
83
|
+
DETAIL: the facts stay the same, a detail was fixed (dates, names,
|
|
84
|
+
sequence order). CORE-CLAIM: what was claimed to have happened
|
|
85
|
+
changed. Core-claim changes are the signal — they mark where the
|
|
86
|
+
account itself shifted.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
kind: str # "detail" | "core-claim"
|
|
90
|
+
text: str
|
|
91
|
+
|
|
92
|
+
def __post_init__(self) -> None:
|
|
93
|
+
if self.kind not in ("detail", "core-claim"):
|
|
94
|
+
raise GroundError(
|
|
95
|
+
f"Correction kind must be 'detail' or 'core-claim', got {self.kind!r}."
|
|
96
|
+
)
|
|
97
|
+
if not self.text.strip():
|
|
98
|
+
raise GroundError("Correction text must not be empty.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class GroundMap:
|
|
103
|
+
"""The audited ground: tensions held, states sequenced, corrections typed."""
|
|
104
|
+
|
|
105
|
+
tensions: list[HeldTension] = field(default_factory=list)
|
|
106
|
+
person_states: list[PersonState] = field(default_factory=list)
|
|
107
|
+
corrections: list[Correction] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
def core_claim_changes(self) -> list[Correction]:
|
|
110
|
+
"""The signal: corrections that changed the account itself."""
|
|
111
|
+
return [c for c in self.corrections if c.kind == "core-claim"]
|
|
112
|
+
|
|
113
|
+
def render(self) -> str:
|
|
114
|
+
lines = ["## Audited ground"]
|
|
115
|
+
for t in self.tensions:
|
|
116
|
+
lines.append(f"- Held tension: {t.text}")
|
|
117
|
+
for p in self.person_states:
|
|
118
|
+
lines.append(f"- State [{p.when}] {p.person}: {p.state}")
|
|
119
|
+
for c in self.corrections:
|
|
120
|
+
lines.append(f"- Correction ({c.kind}): {c.text}")
|
|
121
|
+
if not (self.tensions or self.person_states or self.corrections):
|
|
122
|
+
lines.append("- (no ground mapped)")
|
|
123
|
+
return "\n".join(lines)
|
fairit/hypotheses.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Competing-hypothesis testing — the shared core of this package.
|
|
2
|
+
|
|
3
|
+
Every candidate evasion (shield vocabulary, fogging, DARVO, passive-voice
|
|
4
|
+
responsibility-dodging, whatever dialect it arrives in) is tested as
|
|
5
|
+
competing hypotheses instead of pre-judged:
|
|
6
|
+
|
|
7
|
+
- H_OBFUSCATION: the language functions as armor — deflecting
|
|
8
|
+
responsibility, buying status, or changing the topic.
|
|
9
|
+
- H_GENUINE: the language is a sincere attempt, correlated with
|
|
10
|
+
accountability.
|
|
11
|
+
- H_BOTH: genuine effort that is also conveniently shielding. Both can be
|
|
12
|
+
true — and this is the common case.
|
|
13
|
+
- INSUFFICIENT_EVIDENCE: the honest finding when the evidence cannot
|
|
14
|
+
distinguish the hypotheses. Not a failure.
|
|
15
|
+
|
|
16
|
+
The load-bearing rule, enforced in code: a hypothesis wins ONLY on
|
|
17
|
+
**discriminating evidence** — observations that would differ between the
|
|
18
|
+
hypotheses. Category membership ("this is therapy-speak", "this is
|
|
19
|
+
corporate language") is never evidence. A record that tries to convict
|
|
20
|
+
on category alone is rejected.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
|
|
27
|
+
H_OBFUSCATION = "H-obfuscation"
|
|
28
|
+
H_GENUINE = "H-genuine"
|
|
29
|
+
H_BOTH = "H-both"
|
|
30
|
+
INSUFFICIENT_EVIDENCE = "insufficient-evidence"
|
|
31
|
+
|
|
32
|
+
VERDICTS = (H_OBFUSCATION, H_GENUINE, H_BOTH, INSUFFICIENT_EVIDENCE)
|
|
33
|
+
DIRECTIONED = (H_OBFUSCATION, H_GENUINE, H_BOTH)
|
|
34
|
+
|
|
35
|
+
SUPPORT_STATUSES = ("supported", "contested", "unsupported")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HypothesisError(ValueError):
|
|
39
|
+
"""Raised when a hypothesis record breaks the testing rules."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CategoryPresumptionError(HypothesisError):
|
|
43
|
+
"""Raised when a record tries to convict on category membership alone.
|
|
44
|
+
|
|
45
|
+
The old probe (v1) pre-judged whole categories of language as
|
|
46
|
+
obfuscation. This error is that failure mode, made impossible in code:
|
|
47
|
+
"therapy-speak" by itself, with no observation attached, is not
|
|
48
|
+
discriminating evidence of anything.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Category labels that may contextualize a record but may never stand
|
|
53
|
+
# in for evidence. Lowercase, punctuation-stripped for matching.
|
|
54
|
+
CATEGORY_LABELS = frozenset({
|
|
55
|
+
"therapy speak", "therapy-speak", "therapyspeak", "self regulation",
|
|
56
|
+
"self-regulation", "selfregulation",
|
|
57
|
+
"corporate speak", "corporate-speak", "corporatespeak",
|
|
58
|
+
"bureaucratic", "bureaucratese", "legalese", "wellness speak",
|
|
59
|
+
"wellness-speak", "hr speak", "hr-speak",
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def looks_category_only(text: str) -> bool:
|
|
64
|
+
"""True when an evidence item names a category and observes nothing.
|
|
65
|
+
|
|
66
|
+
Heuristic, deliberately conservative: it catches the bare label
|
|
67
|
+
("therapy-speak") and the label plus filler words ("uses
|
|
68
|
+
therapy-speak", "sounds like corporate speak"). Anything with a real
|
|
69
|
+
observation attached passes — the prose-level judgment ("is this
|
|
70
|
+
actually discriminating?") stays with the operator, and this check
|
|
71
|
+
exists to make the *failure mode* impossible, not to certify the
|
|
72
|
+
good cases.
|
|
73
|
+
"""
|
|
74
|
+
import re
|
|
75
|
+
import string
|
|
76
|
+
|
|
77
|
+
cleaned = text.strip().lower().translate(
|
|
78
|
+
str.maketrans(string.punctuation, " " * len(string.punctuation))
|
|
79
|
+
)
|
|
80
|
+
cleaned = " ".join(cleaned.split())
|
|
81
|
+
if cleaned in CATEGORY_LABELS:
|
|
82
|
+
return True
|
|
83
|
+
for label in CATEGORY_LABELS:
|
|
84
|
+
rest = cleaned.replace(label, " ", 1)
|
|
85
|
+
filler = rest.split()
|
|
86
|
+
# e.g. "uses therapy-speak" / "sounds like corporate speak"
|
|
87
|
+
if len(filler) <= 2 and all(
|
|
88
|
+
w in {"uses", "using", "use", "sounds", "like", "is", "very", "kind",
|
|
89
|
+
"of", "sort", "type", "just", "simply"} for w in filler
|
|
90
|
+
):
|
|
91
|
+
return True
|
|
92
|
+
if re.fullmatch(r"(uses|using|is|sounds like)\s*", rest + " ") or not filler:
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class DiscriminatingEvidence:
|
|
99
|
+
"""One observation that would differ between the hypotheses.
|
|
100
|
+
|
|
101
|
+
`favors` names which hypothesis this observation supports, or
|
|
102
|
+
"neutral" for an observation that constrains the case without
|
|
103
|
+
favoring a side (e.g. a timeline fact both hypotheses must fit).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
text: str
|
|
107
|
+
favors: str = "neutral"
|
|
108
|
+
|
|
109
|
+
def __post_init__(self) -> None:
|
|
110
|
+
if not self.text.strip():
|
|
111
|
+
raise HypothesisError("Evidence item text must not be empty.")
|
|
112
|
+
if self.favors not in DIRECTIONED + ("neutral",):
|
|
113
|
+
raise HypothesisError(f"Bad evidence direction: {self.favors!r}.")
|
|
114
|
+
if looks_category_only(self.text):
|
|
115
|
+
raise CategoryPresumptionError(
|
|
116
|
+
f"Category membership is not evidence: {self.text!r}. "
|
|
117
|
+
"Attach an observation — what was said, done, or omitted, "
|
|
118
|
+
"and what would differ between the hypotheses."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class Mechanism:
|
|
124
|
+
"""One candidate tactic, held as a hypothesis with a support status."""
|
|
125
|
+
|
|
126
|
+
name: str
|
|
127
|
+
status: str # supported | contested | unsupported
|
|
128
|
+
|
|
129
|
+
def __post_init__(self) -> None:
|
|
130
|
+
if self.status not in SUPPORT_STATUSES:
|
|
131
|
+
raise HypothesisError(f"Bad mechanism status: {self.status!r}.")
|
|
132
|
+
if not self.name.strip():
|
|
133
|
+
raise HypothesisError("Mechanism name must not be empty.")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class HypothesisRecord:
|
|
138
|
+
"""The result of testing one candidate evasion.
|
|
139
|
+
|
|
140
|
+
- candidate: the responsibility that may be dodged, or the flagged
|
|
141
|
+
language instance — specific, not a category.
|
|
142
|
+
- verdict: the best-supported hypothesis, or INSUFFICIENT_EVIDENCE.
|
|
143
|
+
- evidence: observations that separate the hypotheses.
|
|
144
|
+
- mechanisms: candidate tactics, each with a support status.
|
|
145
|
+
- remaining_questions: what evidence would resolve the open
|
|
146
|
+
hypotheses. Required when the verdict is INSUFFICIENT_EVIDENCE.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
candidate: str
|
|
150
|
+
verdict: str
|
|
151
|
+
evidence: list[DiscriminatingEvidence] = field(default_factory=list)
|
|
152
|
+
mechanisms: list[Mechanism] = field(default_factory=list)
|
|
153
|
+
remaining_questions: list[str] = field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
if self.verdict not in VERDICTS:
|
|
157
|
+
raise HypothesisError(f"Bad verdict: {self.verdict!r}.")
|
|
158
|
+
if not self.candidate.strip():
|
|
159
|
+
raise HypothesisError("Candidate must name the specific evasion or language instance.")
|
|
160
|
+
self._require_discriminating_evidence()
|
|
161
|
+
|
|
162
|
+
def _favors(self, direction: str) -> list[DiscriminatingEvidence]:
|
|
163
|
+
return [e for e in self.evidence if e.favors == direction]
|
|
164
|
+
|
|
165
|
+
def _require_discriminating_evidence(self) -> None:
|
|
166
|
+
if self.verdict == H_OBFUSCATION and not self._favors(H_OBFUSCATION):
|
|
167
|
+
raise HypothesisError(
|
|
168
|
+
"H-obfuscation requires at least one evidence item favoring "
|
|
169
|
+
"H-obfuscation. Category membership does not count."
|
|
170
|
+
)
|
|
171
|
+
if self.verdict == H_GENUINE and not self._favors(H_GENUINE):
|
|
172
|
+
raise HypothesisError(
|
|
173
|
+
"H-genuine requires at least one evidence item favoring H-genuine."
|
|
174
|
+
)
|
|
175
|
+
if self.verdict == H_BOTH and not (
|
|
176
|
+
self._favors(H_OBFUSCATION) and self._favors(H_GENUINE)
|
|
177
|
+
):
|
|
178
|
+
raise HypothesisError(
|
|
179
|
+
"H-both requires discriminating evidence on BOTH sides — "
|
|
180
|
+
"at least one item favoring H-obfuscation and one favoring "
|
|
181
|
+
"H-genuine. Otherwise the honest verdict is one of the "
|
|
182
|
+
"other three."
|
|
183
|
+
)
|
|
184
|
+
if self.verdict == INSUFFICIENT_EVIDENCE and not self.remaining_questions:
|
|
185
|
+
raise HypothesisError(
|
|
186
|
+
"An insufficient-evidence verdict must name what evidence "
|
|
187
|
+
"would resolve the open hypotheses."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def supports_obfuscation(self) -> bool:
|
|
191
|
+
"""True only for a supported H-obfuscation verdict.
|
|
192
|
+
|
|
193
|
+
This is the single gate for the OBFUSCATION evidence tag.
|
|
194
|
+
Everything else — including suspected-but-untested material —
|
|
195
|
+
stays CLAIM with a hypothesis marker.
|
|
196
|
+
"""
|
|
197
|
+
return self.verdict == H_OBFUSCATION and (
|
|
198
|
+
not self.mechanisms
|
|
199
|
+
or any(m.status == "supported" for m in self.mechanisms)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def summary(self) -> str:
|
|
203
|
+
lines = [
|
|
204
|
+
f"Candidate evasion: {self.candidate}",
|
|
205
|
+
f"Best-supported hypothesis: {self.verdict}",
|
|
206
|
+
"Discriminating evidence:",
|
|
207
|
+
]
|
|
208
|
+
lines.extend(
|
|
209
|
+
f" - [{e.favors}] {e.text}" for e in self.evidence
|
|
210
|
+
) or lines.append(" (none)")
|
|
211
|
+
lines.append("Key mechanisms:")
|
|
212
|
+
lines.extend(
|
|
213
|
+
f" - {m.name} [{m.status}]" for m in self.mechanisms
|
|
214
|
+
) or lines.append(" (none)")
|
|
215
|
+
if self.remaining_questions:
|
|
216
|
+
lines.append("Remaining questions:")
|
|
217
|
+
lines.extend(f" - {q}" for q in self.remaining_questions)
|
|
218
|
+
return "\n".join(lines)
|
fairit/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
|
+
}
|
fairit/overlay.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""The evasion-probe protocol: test, never presume.
|
|
2
|
+
|
|
3
|
+
This is the front-facing form of the two skills being converted together:
|
|
4
|
+
|
|
5
|
+
- responsibility-obfuscation-probe: every candidate evasion becomes
|
|
6
|
+
competing hypotheses tested against discriminating evidence.
|
|
7
|
+
- field-forge's probe stage: the same protocol, generalized past
|
|
8
|
+
therapy-speak to any shield dialect (corporate, bureaucratic).
|
|
9
|
+
|
|
10
|
+
One implementation, two consumers — the overlap the assessment flagged
|
|
11
|
+
is structural, and this module is the single place the protocol lives.
|
|
12
|
+
|
|
13
|
+
The probe's outputs:
|
|
14
|
+
- ObfuscatedObject: the responsibility that MAY be dodged, with a
|
|
15
|
+
support status (supported / contested / unsupported) — never a verdict.
|
|
16
|
+
- JargonFlag: one instance of possibly-evasive vocabulary, run through
|
|
17
|
+
H-obfuscation / H-genuine / H-both against discriminating evidence.
|
|
18
|
+
- The evidence tag for a record: OBFUSCATION only when H-obfuscation is
|
|
19
|
+
supported by discriminating evidence; otherwise CLAIM with a
|
|
20
|
+
hypothesis marker. Suspected-but-untested material is never upgraded
|
|
21
|
+
on category membership alone.
|
|
22
|
+
|
|
23
|
+
The probe never collides, adjudicates, refines, or extracts surprise.
|
|
24
|
+
Its output feeds whatever comes next (the interrogation engine's
|
|
25
|
+
Diverge phase, or the operator's own judgment) as hypothesis-tagged
|
|
26
|
+
material.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
|
|
33
|
+
from .dialects import DIALECTS, detect_markers
|
|
34
|
+
from .evidence import OBFUSCATION, CLAIM, normalize_tag
|
|
35
|
+
from .ledger import Ledger
|
|
36
|
+
from .hypotheses import (
|
|
37
|
+
H_OBFUSCATION,
|
|
38
|
+
H_GENUINE,
|
|
39
|
+
H_BOTH,
|
|
40
|
+
INSUFFICIENT_EVIDENCE,
|
|
41
|
+
SUPPORT_STATUSES,
|
|
42
|
+
DiscriminatingEvidence,
|
|
43
|
+
HypothesisRecord,
|
|
44
|
+
Mechanism,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProbeError(ValueError):
|
|
49
|
+
"""Raised when probe input breaks the protocol."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class JargonFlag:
|
|
54
|
+
"""One instance of possibly-evasive vocabulary, under test.
|
|
55
|
+
|
|
56
|
+
`dialect` names where the shield vocabulary came from; `record`
|
|
57
|
+
carries the actual hypothesis test. A flag with no test attached is
|
|
58
|
+
incomplete — detection without testing is exactly the v1 failure
|
|
59
|
+
mode this package exists to prevent.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
phrase: str
|
|
63
|
+
dialect: str
|
|
64
|
+
record: HypothesisRecord
|
|
65
|
+
|
|
66
|
+
def __post_init__(self) -> None:
|
|
67
|
+
if not self.phrase.strip():
|
|
68
|
+
raise ProbeError("Jargon flag needs the flagged phrase.")
|
|
69
|
+
if self.dialect not in DIALECTS:
|
|
70
|
+
raise ProbeError(
|
|
71
|
+
f"Unknown dialect {self.dialect!r}. Known: {', '.join(DIALECTS)}."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class ObfuscatedObject:
|
|
77
|
+
"""The responsibility that may be dodged — a hypothesis, not a verdict."""
|
|
78
|
+
|
|
79
|
+
candidate: str
|
|
80
|
+
support: str # supported | contested | unsupported
|
|
81
|
+
apparent_function: str = "" # what the evasion would achieve if real
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
if not self.candidate.strip():
|
|
85
|
+
raise ProbeError("Obfuscated-object candidate must not be empty.")
|
|
86
|
+
if self.support not in SUPPORT_STATUSES:
|
|
87
|
+
raise ProbeError(f"Bad support status: {self.support!r}.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_jargon_test(
|
|
91
|
+
phrase: str,
|
|
92
|
+
dialect: str,
|
|
93
|
+
evidence_for_obfuscation: str,
|
|
94
|
+
evidence_for_genuine: str,
|
|
95
|
+
*,
|
|
96
|
+
mechanisms: list[Mechanism] | None = None,
|
|
97
|
+
remaining_questions: list[str] | None = None,
|
|
98
|
+
) -> JargonFlag:
|
|
99
|
+
"""Run one jargon instance through the hypothesis protocol.
|
|
100
|
+
|
|
101
|
+
Both hypotheses are tested against discriminating evidence. When the
|
|
102
|
+
evidence favors one side, that hypothesis wins; when it favors both,
|
|
103
|
+
the honest verdict is H-both; when it can't distinguish them, the
|
|
104
|
+
honest verdict is insufficient-evidence — never a verdict.
|
|
105
|
+
"""
|
|
106
|
+
has_obf = bool(evidence_for_obfuscation.strip())
|
|
107
|
+
has_gen = bool(evidence_for_genuine.strip())
|
|
108
|
+
if has_obf and not has_gen:
|
|
109
|
+
verdict = H_OBFUSCATION
|
|
110
|
+
evidence = [DiscriminatingEvidence(evidence_for_obfuscation, H_OBFUSCATION)]
|
|
111
|
+
elif has_gen and not has_obf:
|
|
112
|
+
verdict = H_GENUINE
|
|
113
|
+
evidence = [DiscriminatingEvidence(evidence_for_genuine, H_GENUINE)]
|
|
114
|
+
elif has_obf and has_gen:
|
|
115
|
+
verdict = H_BOTH
|
|
116
|
+
evidence = [
|
|
117
|
+
DiscriminatingEvidence(evidence_for_obfuscation, H_OBFUSCATION),
|
|
118
|
+
DiscriminatingEvidence(evidence_for_genuine, H_GENUINE),
|
|
119
|
+
]
|
|
120
|
+
else:
|
|
121
|
+
verdict = INSUFFICIENT_EVIDENCE
|
|
122
|
+
evidence = []
|
|
123
|
+
if not remaining_questions:
|
|
124
|
+
remaining_questions = [
|
|
125
|
+
"What observation would distinguish H-obfuscation from "
|
|
126
|
+
"H-genuine here? (e.g. does the language appear only under "
|
|
127
|
+
"pressure? is it followed by accountability or topic change?)"
|
|
128
|
+
]
|
|
129
|
+
record = HypothesisRecord(
|
|
130
|
+
candidate=f"the phrase {phrase!r} may function as evasion",
|
|
131
|
+
verdict=verdict,
|
|
132
|
+
evidence=evidence,
|
|
133
|
+
mechanisms=list(mechanisms or []),
|
|
134
|
+
remaining_questions=list(remaining_questions or []),
|
|
135
|
+
)
|
|
136
|
+
Ledger("fairit").record(
|
|
137
|
+
"jargon_test",
|
|
138
|
+
{
|
|
139
|
+
"dialect": dialect,
|
|
140
|
+
"verdict": verdict,
|
|
141
|
+
"mechanisms": len(mechanisms or []),
|
|
142
|
+
"evidence_items": len(evidence),
|
|
143
|
+
},
|
|
144
|
+
input_text=phrase,
|
|
145
|
+
)
|
|
146
|
+
return JargonFlag(phrase=phrase, dialect=dialect, record=record)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def scan_and_test(
|
|
150
|
+
text: str,
|
|
151
|
+
evidence: dict[str, tuple[str, str]],
|
|
152
|
+
) -> list[JargonFlag]:
|
|
153
|
+
"""Detect shield vocabulary in text and run each hit through the protocol.
|
|
154
|
+
|
|
155
|
+
`evidence` maps each detected phrase to
|
|
156
|
+
(evidence_for_obfuscation, evidence_for_genuine). A hit with no
|
|
157
|
+
supplied evidence gets the insufficient-evidence verdict — the probe
|
|
158
|
+
refuses to test-free convict OR exonerate on detection alone.
|
|
159
|
+
"""
|
|
160
|
+
flags: list[JargonFlag] = []
|
|
161
|
+
for dialect, phrase in detect_markers(text):
|
|
162
|
+
for_obf, for_gen = evidence.get(phrase, ("", ""))
|
|
163
|
+
flags.append(
|
|
164
|
+
run_jargon_test(
|
|
165
|
+
phrase, dialect, for_obf, for_gen,
|
|
166
|
+
remaining_questions=[
|
|
167
|
+
"What evidence would distinguish obfuscation from "
|
|
168
|
+
"genuine use here?"
|
|
169
|
+
] if not (for_obf.strip() or for_gen.strip()) else [],
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return flags
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def evidence_tag_for(record: HypothesisRecord, *, probe_ran: bool = True) -> str:
|
|
176
|
+
"""The evidence tag a record earns.
|
|
177
|
+
|
|
178
|
+
OBFUSCATION only when the probe ran and H-obfuscation is supported
|
|
179
|
+
by discriminating evidence. Everything else stays CLAIM — with a
|
|
180
|
+
hypothesis marker, so downstream consumers know a test was run.
|
|
181
|
+
"""
|
|
182
|
+
if probe_ran and record.supports_obfuscation():
|
|
183
|
+
return normalize_tag(OBFUSCATION, probe_ran=True)
|
|
184
|
+
return normalize_tag(CLAIM)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fairit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Ground mapping and evasion-hypothesis testing: hold the state, test the language, hand off the seeds. Never presumes evasion.
|
|
5
|
+
Author: Ryan Wesley Hitchcock
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ohpersonperson/frontface
|
|
8
|
+
Keywords: hypothesis-testing,evasion,audit,interrogation,evidence
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# fairit — ground mapping and evasion-hypothesis testing
|
|
18
|
+
|
|
19
|
+
Tests whether evasive-looking communication is actually evading
|
|
20
|
+
anything — and if so, what. It never presumes evasion. Three stages:
|
|
21
|
+
|
|
22
|
+
1. **Ground mapping.** Establish the exact situational state without
|
|
23
|
+
forcing narrative resolution: opposing facts as parallel,
|
|
24
|
+
non-subordinating sentences ("X is true. Y is also true." — never
|
|
25
|
+
"X, but Y"), people as time-indexed states, every correction typed
|
|
26
|
+
as detail vs core-claim.
|
|
27
|
+
2. **Evasion-hypothesis testing.** Every candidate evasion — therapy-speak,
|
|
28
|
+
corporate-speak, bureaucratic passive voice, fogging, DARVO,
|
|
29
|
+
projection — becomes competing hypotheses: **H-obfuscation /
|
|
30
|
+
H-genuine / H-both**, tested against discriminating evidence.
|
|
31
|
+
3. **Handoff.** 2–3 seed frames plus the audited ground, handed to
|
|
32
|
+
whatever comes next. Then the package stands down.
|
|
33
|
+
|
|
34
|
+
The one rule everything else serves: **a hypothesis wins only on
|
|
35
|
+
discriminating evidence** — observations that would differ between the
|
|
36
|
+
hypotheses. Category membership ("this is therapy-speak") is never
|
|
37
|
+
evidence. "Insufficient evidence to distinguish" is an honest finding,
|
|
38
|
+
not a failure.
|
|
39
|
+
|
|
40
|
+
No prerequisites. The mechanism is the whole product. No model, no
|
|
41
|
+
network — this is pure machinery. The operator (human or model) supplies
|
|
42
|
+
the evidence; the library enforces the testing discipline.
|
|
43
|
+
|
|
44
|
+
*(Lineage: the front-facing conversion of two skills from Ryan's
|
|
45
|
+
skill-suite, MIT — `responsibility-obfuscation-probe` and `field-forge`,
|
|
46
|
+
converted as one unit because they share one hypothesis-testing core.
|
|
47
|
+
What changed in the conversion is documented in CHANGELOG.md.)*
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from fairit import scan_and_test, evidence_tag_for
|
|
53
|
+
|
|
54
|
+
text = "Mistakes were made in the scoping phase. We're going to circle back."
|
|
55
|
+
flags = scan_and_test(text, {
|
|
56
|
+
"mistakes were made": (
|
|
57
|
+
"the passive phrasing names no actor; internal emails name the PM "
|
|
58
|
+
"who cut scoping short — the actor is known",
|
|
59
|
+
"",
|
|
60
|
+
),
|
|
61
|
+
"circle back": (
|
|
62
|
+
"used to end the paragraph about the miss without giving a new date",
|
|
63
|
+
"",
|
|
64
|
+
),
|
|
65
|
+
})
|
|
66
|
+
for flag in flags:
|
|
67
|
+
print(flag.phrase, "->", flag.record.verdict, "->", evidence_tag_for(flag.record))
|
|
68
|
+
# mistakes were made -> H-obfuscation -> OBFUSCATION
|
|
69
|
+
# circle back -> H-obfuscation -> OBFUSCATION
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Full cycle demo (ground → probe → brief), no model needed:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd probe-forge
|
|
76
|
+
PYTHONPATH=src python3 examples/run_cycle.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What it does
|
|
80
|
+
|
|
81
|
+
**Ground mapping** (`ground.py`) — the pre-interrogation audit:
|
|
82
|
+
|
|
83
|
+
- Held tensions must be parallel non-subordinating sentences. The
|
|
84
|
+
subordinating-conjunction scan is in code: "promised a refund, *but*
|
|
85
|
+
none arrived" is rejected; "promised a refund. None arrived." passes.
|
|
86
|
+
- People are time-indexed states, never one flat characterization.
|
|
87
|
+
- Corrections are typed detail vs core-claim. Core-claim changes — the
|
|
88
|
+
account itself shifting — are the signal, and the library surfaces
|
|
89
|
+
them.
|
|
90
|
+
|
|
91
|
+
**Hypothesis testing** (`hypotheses.py`) — the shared core:
|
|
92
|
+
|
|
93
|
+
- Four verdicts: H-obfuscation, H-genuine, H-both, insufficient-evidence.
|
|
94
|
+
- Code-enforced rules: H-obfuscation needs ≥1 evidence item favoring it;
|
|
95
|
+
H-both needs evidence on *both* sides; insufficient-evidence must name
|
|
96
|
+
what would resolve the open hypotheses.
|
|
97
|
+
- The category-presumption check: evidence that only names a category
|
|
98
|
+
("therapy-speak", "uses corporate speak") is rejected with
|
|
99
|
+
`CategoryPresumptionError`. This is the old probe's failure mode
|
|
100
|
+
(pre-judging therapy-speak as armor), made impossible in code.
|
|
101
|
+
|
|
102
|
+
**Probe protocol** (`overlay.py`) — jargon in three dialects (therapy,
|
|
103
|
+
corporate, bureaucratic; see `dialects.py`):
|
|
104
|
+
|
|
105
|
+
- Detection produces *candidates*, never verdicts. A detected phrase with
|
|
106
|
+
no evidence gets insufficient-evidence — the probe refuses to convict
|
|
107
|
+
*or* exonerate on detection alone.
|
|
108
|
+
- The evidence tag: **OBFUSCATION** only when H-obfuscation is supported
|
|
109
|
+
by discriminating evidence. Everything else stays **CLAIM** with a
|
|
110
|
+
hypothesis marker. Suspected-but-untested material is never upgraded
|
|
111
|
+
on category membership alone.
|
|
112
|
+
|
|
113
|
+
**Audit brief** (`brief.py`) — the output artifact with frontmatter
|
|
114
|
+
(`artifact:`, `date:`, `protocol:`, `lifecycle:`), Markdown
|
|
115
|
+
render/parse, and the stand-down rule enforced in code: the brief must
|
|
116
|
+
never contain collision, adjudication, refinement, or surprise sections.
|
|
117
|
+
Those belong to the interrogation engine. If the engine isn't running,
|
|
118
|
+
the brief is reported and the work stops.
|
|
119
|
+
|
|
120
|
+
**Evidence taxonomy** (`evidence.py`) — one authoritative mapping.
|
|
121
|
+
field-forge shipped an 8-tier list; the interrogation engine uses 11
|
|
122
|
+
tags (10 core + overlay-only OBFUSCATION). The forge's 8 map 1:1 onto
|
|
123
|
+
the engine's; the 3 the forge never named (HYPOTHESIS, REQUIREMENT,
|
|
124
|
+
DEPENDENCY) are documented as recognized. `check_matches_engine()`
|
|
125
|
+
guards against silent forks when the sibling package is importable.
|
|
126
|
+
|
|
127
|
+
## What's honest about the limits
|
|
128
|
+
|
|
129
|
+
- The library cannot prove a counter-hypothesis is *strong* — the
|
|
130
|
+
evidence-quality judgment stays with the operator. What it makes
|
|
131
|
+
impossible is the failure modes: verdicts without discriminating
|
|
132
|
+
evidence, category-based presumption, silent upgrades to OBFUSCATION,
|
|
133
|
+
subordinated tensions, and the overlay doing the engine's job.
|
|
134
|
+
- The dialog procedure (asking for raw input, precision-forcing
|
|
135
|
+
questions) is an operator protocol, documented in the examples — it
|
|
136
|
+
isn't codeable without a model in the loop, and isn't pretended to be.
|
|
137
|
+
|
|
138
|
+
## Tests
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
55 offline tests: verdict rules, category-presumption rejection (the v1
|
|
145
|
+
regression suite), the OBFUSCATION gate, the subordinator scan,
|
|
146
|
+
correction typing, taxonomy reconciliation, brief round-trip and
|
|
147
|
+
stand-down, dialect detection, and a full ground→probe→brief cycle.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
fairit/__init__.py,sha256=AI34I3fLf4eVUQJAWDTmN3_dqbVBUV7Jhj_7y_QiDpQ,1869
|
|
2
|
+
fairit/brief.py,sha256=7xKpMgRPGK1XsFo201BpN3me_BIIUg7CfJmMZ6fXodc,4903
|
|
3
|
+
fairit/dialects.py,sha256=yY8S4U28TJ4VK9VyJ2st5DHbk-eBU-06Hz18DiLqg24,2136
|
|
4
|
+
fairit/evidence.py,sha256=NxQhEkinxy96d_KhsFDjTNdNgtuuQPzm0ixRUf4AsGc,6854
|
|
5
|
+
fairit/ground.py,sha256=-U6vh9buouy3s5NTRq0z54gKL3yT0oC0TMP7BA_hFw0,4335
|
|
6
|
+
fairit/hypotheses.py,sha256=RVoOqDpnkK-pBSYdrp3_guUAxF3klC94hS-_vAqBdPI,8749
|
|
7
|
+
fairit/ledger.py,sha256=x8TH2et1Jt_UTzLMn49R8YKEiMMykYTW6UoMz-l8qCU,4245
|
|
8
|
+
fairit/overlay.py,sha256=EgF5AB82gF778e3zZdvOb3WEcmaEoJGnB4rC13NijkY,6713
|
|
9
|
+
fairit-1.0.0.dist-info/licenses/LICENSE,sha256=Zx_XafyZDq7qBexUb7tER9kYlBZ0rCVaAwKuqMCfxLE,1078
|
|
10
|
+
fairit-1.0.0.dist-info/METADATA,sha256=cQ0JYmymqqmFucs45s0HEBnuZbKkEKFqhIp5Wrb0s78,6496
|
|
11
|
+
fairit-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
fairit-1.0.0.dist-info/top_level.txt,sha256=3abimNla0_0zZJ5Cj3UWU_9zUMRQx44En85ySkDCmkg,7
|
|
13
|
+
fairit-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Wesley Hitchcock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fairit
|