pressit 1.0.0__tar.gz

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.
pressit-1.0.0/LICENSE ADDED
@@ -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.
pressit-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: pressit
3
+ Version: 1.0.0
4
+ Summary: Structured adversarial interrogation of contradictory or uncertain fields (IFS: Iterative Field Synthesis)
5
+ Author: Ryan Wesley Hitchcock
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ohpersonperson/frontface
8
+ Keywords: interrogation,adversarial-reasoning,evidence,red-teaming,decision-analysis
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
+ # IFS — Iterative Field Synthesis
18
+
19
+ A structured way to pressure-test a claim, decision, or contradiction so it comes out the other side either **load-bearing** or **dead** — with the reasoning preserved in a portable document.
20
+
21
+ No prerequisites. You don't need to know anything about the project's history, any framework, or any belief system. The mechanism is the whole product.
22
+
23
+ *(Lineage: this is the front-facing conversion of the `ifs-interrogation` skill from Ryan's skill-suite, MIT. What changed in the conversion is documented in CHANGELOG.md — mechanism kept, original naming removed.)*
24
+
25
+ ## What it does
26
+
27
+ You hand the engine a **field** — a situation, claim, or contradiction with real stakes and real uncertainty. The engine runs it through a fixed interrogation:
28
+
29
+ 1. **Identify & Decompose** — bound the field, name the objective and scope, tag every input with an evidence tier (FACT, CLAIM, UNKNOWN, …). Evidence is never silently upgraded: a claim stays a claim until it's proven.
30
+ 2. **Diverge** — build the 2 or 3 strongest internally-coherent cases (Takes). No strawmen, no premature consensus.
31
+ 3. **Collide** — force the Takes into conflict. Every collision names the contradiction, the premise that must break, and the discriminator evidence that decides it. If the collision doesn't hurt brittle claims, it wasn't a real collision.
32
+ 4. **Refine** — a skeptic pass, then extract 3–5 **load-bearing Keys**: crisp assertions, each with a classification, the tagged evidence behind it, a confidence level, its structural vulnerability, and its falsifier — the specific evidence that would overturn it. A Key that can't survive is discarded, not softened.
33
+ 5. **Surprise + Synthesis + Capture** — the non-obvious result that emerged *from the collision* (not from one Take), the surviving model, and the complete **state artifact**: a self-contained Markdown document another person or model can continue from without any of the original conversation.
34
+
35
+ Core principle: **confidence and correctness are independent variables.** The engine interrogates the gap between them.
36
+
37
+ ## Why it works
38
+
39
+ Most reasoning failures aren't stupidity — they're structural. Claims quietly upgrade into facts. The first plausible story wins and alternatives never get built. Contradictions get smoothed over with "however." IFS attacks all three mechanically:
40
+
41
+ - **Evidence tagging** makes silent upgrades impossible — every input carries its tier through every phase.
42
+ - **Forced Takes** mean the strongest opposing case gets built with the same care as your own.
43
+ - **Named collisions with discriminators** force a decision rule instead of vibes.
44
+ - **Keys carry falsifiers**, so the output tells you exactly what would prove it wrong.
45
+ - **Prior Keys are stress-tested, never inherited.** In session 2+, every old Key gets a verdict — HELD, CRACKED, MODIFIED, SUPERSEDED, UNRESOLVED — and failures stay visible in the artifact instead of being quietly rewritten.
46
+
47
+ ## Quickstart
48
+
49
+ Zero dependencies beyond Python 3.10+ and a model to run the engine on.
50
+
51
+ ```bash
52
+ cd ifs
53
+ python3 -m unittest discover -s tests # 41 offline tests — prove the machinery first
54
+ ```
55
+
56
+ To run a real interrogation, you need a backend — any LLM that can follow the protocol prompt:
57
+
58
+ ```bash
59
+ # Fully offline: local Ollama
60
+ python3 examples/run_local.py my-field.txt --ollama
61
+
62
+ # OpenRouter (free-tier models work fine)
63
+ OPENROUTER_API_KEY=... OPENROUTER_MODEL="nvidia/nemotron-3-ultra-550b-a55b:free" \
64
+ python3 examples/run_local.py my-field.txt
65
+
66
+ # Session 2+: feed the prior artifact back in; old Keys get stress-tested
67
+ python3 examples/run_local.py new-evidence.txt --prior interrogation-state-my-field.md --ollama
68
+ ```
69
+
70
+ `my-field.txt` is just prose: the situation, the conflicting accounts, the stakes. The engine returns `interrogation-state-<slug>.md` — the portable artifact.
71
+
72
+ Or use it as a library:
73
+
74
+ ```python
75
+ from pressit import FieldInput, EvidenceItem, run, OllamaBackend
76
+
77
+ field = FieldInput(
78
+ field="Q3 revenue is up 41%. The owner calls it a turnaround. Is it?",
79
+ evidence=[
80
+ EvidenceItem("Q3 revenue $48k; Q2 $34k (POS records)", "FACT"),
81
+ EvidenceItem("A July estate sale moved ~2,100 rare books", "FACT"),
82
+ EvidenceItem("Owner: 'new regulars are driving it'", "CLAIM"),
83
+ EvidenceItem("Q4 inventory pipeline", "UNKNOWN"),
84
+ ],
85
+ )
86
+ result = run(field, OllamaBackend())
87
+ assert result.ok
88
+ print(result.artifact["surprise"])
89
+ ```
90
+
91
+ ## The four modes
92
+
93
+ | Mode | When |
94
+ |---|---|
95
+ | `standard` (default) | Bounded fields: decisions, claims, single contradictions. One pass, never loops. |
96
+ | `systemic` | Structural/multi-actor fields, recurring patterns. Nine stages; may iterate across sessions. |
97
+ | `stress-test` | Prior state exists. Every prior Key is dispositioned; only genuinely new Keys are added. |
98
+ | `adjudication` | A claim is judged against an explicit standard (a contract, a policy, a spec). Full deep run, no compression. |
99
+
100
+ The optional **evasion-probe overlay** (off by default) handles interpersonal fields where language may be doing evasive work: it maps the observable ground, tests evasion hypotheses (never presumes them — jargon is tested as H-obfuscation / H-genuine / H-both), and feeds Take seeds into Diverge. It never replaces the engine.
101
+
102
+ ## Inputs, outputs
103
+
104
+ - **Input:** a field description (prose), optional tagged evidence, optional prior artifact, optional overlay.
105
+ - **Output:** a normalized artifact dict, or a rendered `interrogation-state-<slug>.md` — complete, self-contained, no placeholders, no TODOs.
106
+ - **The engine never writes files and never claims external persistence.** Your code saves the artifact; the engine's job ends when it's emitted. That's the persistence boundary, and it's deliberate.
107
+
108
+ ## Worked examples
109
+
110
+ Three complete runs a stranger can follow, in `examples/`:
111
+
112
+ 1. `example-1-corner-books.md` — standard one-pass: is a bookstore's revenue jump a turnaround or a blip?
113
+ 2. `example-2-corner-books-session-2.md` — stress-test mode: new evidence meets old Keys; watch a Key get CRACKED and preserved visibly.
114
+ 3. `example-3-sla-adjudication.md` — adjudication mode: a vendor's SLA claim judged against the contract text.
115
+
116
+ *All three fields are constructed for teaching — they show the engine's shape, not real findings.*
117
+
118
+ ## FAQ
119
+
120
+ **Is this related to Internal Family Systems therapy?** No. The acronym collision is coincidental; this is an interrogation engine, not a therapeutic method.
121
+
122
+ **Does it need an internet connection?** Only for the model call, and only the backend you choose. With Ollama it's fully offline. The 41 unit tests never touch the network.
123
+
124
+ **What does it cost?** The package is free and dependency-free. The model call costs whatever your provider charges — free-tier models work; the engine was evaluated on them.
125
+
126
+ ## License
127
+
128
+ MIT. See LICENSE.
@@ -0,0 +1,112 @@
1
+ # IFS — Iterative Field Synthesis
2
+
3
+ A structured way to pressure-test a claim, decision, or contradiction so it comes out the other side either **load-bearing** or **dead** — with the reasoning preserved in a portable document.
4
+
5
+ No prerequisites. You don't need to know anything about the project's history, any framework, or any belief system. The mechanism is the whole product.
6
+
7
+ *(Lineage: this is the front-facing conversion of the `ifs-interrogation` skill from Ryan's skill-suite, MIT. What changed in the conversion is documented in CHANGELOG.md — mechanism kept, original naming removed.)*
8
+
9
+ ## What it does
10
+
11
+ You hand the engine a **field** — a situation, claim, or contradiction with real stakes and real uncertainty. The engine runs it through a fixed interrogation:
12
+
13
+ 1. **Identify & Decompose** — bound the field, name the objective and scope, tag every input with an evidence tier (FACT, CLAIM, UNKNOWN, …). Evidence is never silently upgraded: a claim stays a claim until it's proven.
14
+ 2. **Diverge** — build the 2 or 3 strongest internally-coherent cases (Takes). No strawmen, no premature consensus.
15
+ 3. **Collide** — force the Takes into conflict. Every collision names the contradiction, the premise that must break, and the discriminator evidence that decides it. If the collision doesn't hurt brittle claims, it wasn't a real collision.
16
+ 4. **Refine** — a skeptic pass, then extract 3–5 **load-bearing Keys**: crisp assertions, each with a classification, the tagged evidence behind it, a confidence level, its structural vulnerability, and its falsifier — the specific evidence that would overturn it. A Key that can't survive is discarded, not softened.
17
+ 5. **Surprise + Synthesis + Capture** — the non-obvious result that emerged *from the collision* (not from one Take), the surviving model, and the complete **state artifact**: a self-contained Markdown document another person or model can continue from without any of the original conversation.
18
+
19
+ Core principle: **confidence and correctness are independent variables.** The engine interrogates the gap between them.
20
+
21
+ ## Why it works
22
+
23
+ Most reasoning failures aren't stupidity — they're structural. Claims quietly upgrade into facts. The first plausible story wins and alternatives never get built. Contradictions get smoothed over with "however." IFS attacks all three mechanically:
24
+
25
+ - **Evidence tagging** makes silent upgrades impossible — every input carries its tier through every phase.
26
+ - **Forced Takes** mean the strongest opposing case gets built with the same care as your own.
27
+ - **Named collisions with discriminators** force a decision rule instead of vibes.
28
+ - **Keys carry falsifiers**, so the output tells you exactly what would prove it wrong.
29
+ - **Prior Keys are stress-tested, never inherited.** In session 2+, every old Key gets a verdict — HELD, CRACKED, MODIFIED, SUPERSEDED, UNRESOLVED — and failures stay visible in the artifact instead of being quietly rewritten.
30
+
31
+ ## Quickstart
32
+
33
+ Zero dependencies beyond Python 3.10+ and a model to run the engine on.
34
+
35
+ ```bash
36
+ cd ifs
37
+ python3 -m unittest discover -s tests # 41 offline tests — prove the machinery first
38
+ ```
39
+
40
+ To run a real interrogation, you need a backend — any LLM that can follow the protocol prompt:
41
+
42
+ ```bash
43
+ # Fully offline: local Ollama
44
+ python3 examples/run_local.py my-field.txt --ollama
45
+
46
+ # OpenRouter (free-tier models work fine)
47
+ OPENROUTER_API_KEY=... OPENROUTER_MODEL="nvidia/nemotron-3-ultra-550b-a55b:free" \
48
+ python3 examples/run_local.py my-field.txt
49
+
50
+ # Session 2+: feed the prior artifact back in; old Keys get stress-tested
51
+ python3 examples/run_local.py new-evidence.txt --prior interrogation-state-my-field.md --ollama
52
+ ```
53
+
54
+ `my-field.txt` is just prose: the situation, the conflicting accounts, the stakes. The engine returns `interrogation-state-<slug>.md` — the portable artifact.
55
+
56
+ Or use it as a library:
57
+
58
+ ```python
59
+ from pressit import FieldInput, EvidenceItem, run, OllamaBackend
60
+
61
+ field = FieldInput(
62
+ field="Q3 revenue is up 41%. The owner calls it a turnaround. Is it?",
63
+ evidence=[
64
+ EvidenceItem("Q3 revenue $48k; Q2 $34k (POS records)", "FACT"),
65
+ EvidenceItem("A July estate sale moved ~2,100 rare books", "FACT"),
66
+ EvidenceItem("Owner: 'new regulars are driving it'", "CLAIM"),
67
+ EvidenceItem("Q4 inventory pipeline", "UNKNOWN"),
68
+ ],
69
+ )
70
+ result = run(field, OllamaBackend())
71
+ assert result.ok
72
+ print(result.artifact["surprise"])
73
+ ```
74
+
75
+ ## The four modes
76
+
77
+ | Mode | When |
78
+ |---|---|
79
+ | `standard` (default) | Bounded fields: decisions, claims, single contradictions. One pass, never loops. |
80
+ | `systemic` | Structural/multi-actor fields, recurring patterns. Nine stages; may iterate across sessions. |
81
+ | `stress-test` | Prior state exists. Every prior Key is dispositioned; only genuinely new Keys are added. |
82
+ | `adjudication` | A claim is judged against an explicit standard (a contract, a policy, a spec). Full deep run, no compression. |
83
+
84
+ The optional **evasion-probe overlay** (off by default) handles interpersonal fields where language may be doing evasive work: it maps the observable ground, tests evasion hypotheses (never presumes them — jargon is tested as H-obfuscation / H-genuine / H-both), and feeds Take seeds into Diverge. It never replaces the engine.
85
+
86
+ ## Inputs, outputs
87
+
88
+ - **Input:** a field description (prose), optional tagged evidence, optional prior artifact, optional overlay.
89
+ - **Output:** a normalized artifact dict, or a rendered `interrogation-state-<slug>.md` — complete, self-contained, no placeholders, no TODOs.
90
+ - **The engine never writes files and never claims external persistence.** Your code saves the artifact; the engine's job ends when it's emitted. That's the persistence boundary, and it's deliberate.
91
+
92
+ ## Worked examples
93
+
94
+ Three complete runs a stranger can follow, in `examples/`:
95
+
96
+ 1. `example-1-corner-books.md` — standard one-pass: is a bookstore's revenue jump a turnaround or a blip?
97
+ 2. `example-2-corner-books-session-2.md` — stress-test mode: new evidence meets old Keys; watch a Key get CRACKED and preserved visibly.
98
+ 3. `example-3-sla-adjudication.md` — adjudication mode: a vendor's SLA claim judged against the contract text.
99
+
100
+ *All three fields are constructed for teaching — they show the engine's shape, not real findings.*
101
+
102
+ ## FAQ
103
+
104
+ **Is this related to Internal Family Systems therapy?** No. The acronym collision is coincidental; this is an interrogation engine, not a therapeutic method.
105
+
106
+ **Does it need an internet connection?** Only for the model call, and only the backend you choose. With Ollama it's fully offline. The 41 unit tests never touch the network.
107
+
108
+ **What does it cost?** The package is free and dependency-free. The model call costs whatever your provider charges — free-tier models work; the engine was evaluated on them.
109
+
110
+ ## License
111
+
112
+ MIT. See LICENSE.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pressit"
7
+ version = "1.0.0"
8
+ description = "Structured adversarial interrogation of contradictory or uncertain fields (IFS: Iterative Field Synthesis)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Ryan Wesley Hitchcock" }]
13
+ keywords = ["interrogation", "adversarial-reasoning", "evidence", "red-teaming", "decision-analysis"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/ohpersonperson/frontface"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.pytest.ini_options]
27
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ """pressit — front-facing conversion of the IFS interrogation engine.
2
+
3
+ IFS = Iterative Field Synthesis: structured adversarial interrogation of
4
+ contradictory or uncertain fields. See README.md for the plain-language
5
+ version. See CHANGELOG.md for what was decoupled from the original skill.
6
+ """
7
+
8
+ from . import artifact, backends, engine, evidence, overlay, protocol
9
+ from .artifact import (
10
+ StateArtifact, Take, Collision, Synthesis, PriorKeyEvaluation,
11
+ slugify_field, render, render_frontmatter, check_sections, load_prior,
12
+ )
13
+ from .backends import StubBackend, OpenRouterBackend, OllamaBackend
14
+ from .engine import LLMBackend, RunResult, build_user_message, parse_model_json, normalize, run
15
+ from .evidence import EvidenceItem, TAXONOMY, normalize_tag, EvidenceError
16
+ from .overlay import (
17
+ ProbeBrief, JargonFlag, ground, probe_jargon, handoff_seeds,
18
+ H_OBFUSCATION, H_GENUINE, H_BOTH, H_INSUFFICIENT,
19
+ )
20
+ from .protocol import (
21
+ SYSTEM_PROMPT, FieldInput, KeyDraft, ProtocolError,
22
+ required_collision_pairs, validate_keys,
23
+ STANDARD, SYSTEMIC, STRESS_TEST, ADJUDICATION,
24
+ )
25
+
26
+ __version__ = "1.0.0"
27
+ __all__ = [
28
+ "artifact", "backends", "engine", "evidence", "overlay", "protocol",
29
+ "StateArtifact", "Take", "Collision", "Synthesis", "PriorKeyEvaluation",
30
+ "slugify_field", "render", "render_frontmatter", "check_sections", "load_prior",
31
+ "StubBackend", "OpenRouterBackend", "OllamaBackend",
32
+ "LLMBackend", "RunResult", "build_user_message", "parse_model_json",
33
+ "normalize", "run",
34
+ "EvidenceItem", "TAXONOMY", "normalize_tag", "EvidenceError",
35
+ "ProbeBrief", "JargonFlag", "ground", "probe_jargon", "handoff_seeds",
36
+ "H_OBFUSCATION", "H_GENUINE", "H_BOTH", "H_INSUFFICIENT",
37
+ "SYSTEM_PROMPT", "FieldInput", "KeyDraft", "ProtocolError",
38
+ "required_collision_pairs", "validate_keys",
39
+ "STANDARD", "SYSTEMIC", "STRESS_TEST", "ADJUDICATION",
40
+ ]
@@ -0,0 +1,248 @@
1
+ """The state artifact: the portable, self-contained deliverable of an
2
+ interrogation.
3
+
4
+ One field, one canonical current state file, named
5
+ ``interrogation-state-[field-slug].md``. Another person (or model) must be
6
+ able to continue the investigation from the artifact alone — never
7
+ "see above", never "as discussed", never unstated context, never memory.
8
+
9
+ Persistence boundary: the engine's responsibility ends when the complete
10
+ artifact has been emitted. The *caller* saves it to disk and supplies it to
11
+ future interrogations. The engine never writes files and never claims
12
+ external persistence occurred.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from dataclasses import dataclass, field
19
+
20
+ from .protocol import (
21
+ KEY_CLASSIFICATIONS, CONFIDENCES, PRIOR_DISPOSITIONS,
22
+ SESSION_LIFECYCLES, PROTOCOL_NAME, PROTOCOL_VERSION,
23
+ required_collision_pairs, validate_keys, KeyDraft,
24
+ )
25
+
26
+ SCHEMA_SECTIONS = (
27
+ "Interrogation Metadata", "Prior State", "1. Identify", "2. Decompose",
28
+ "3. Diverge", "4. Collide", "5. Refine", "6. Surprise",
29
+ "7. Synthesize", "State Status",
30
+ )
31
+
32
+
33
+ class ArtifactError(ValueError):
34
+ """Raised when an artifact breaks the canonical schema."""
35
+
36
+
37
+ @dataclass
38
+ class PriorKeyEvaluation:
39
+ statement: str
40
+ status: str # HELD | CRACKED | MODIFIED | SUPERSEDED | UNRESOLVED
41
+ note: str
42
+
43
+ def __post_init__(self) -> None:
44
+ status = self.status.strip().upper()
45
+ if status not in PRIOR_DISPOSITIONS:
46
+ raise ArtifactError(f"Bad prior-Key disposition: {self.status!r}.")
47
+ self.status = status
48
+
49
+
50
+ @dataclass
51
+ class Take:
52
+ id: str
53
+ title: str
54
+ argument: str
55
+
56
+
57
+ @dataclass
58
+ class Collision:
59
+ pair: str
60
+ contradiction: str
61
+ premise_failure: str
62
+ discriminator: str
63
+
64
+
65
+ @dataclass
66
+ class Synthesis:
67
+ established_ground: str
68
+ surviving_model: str
69
+ remaining_uncertainties: str
70
+ primary_next_target: str
71
+
72
+
73
+ @dataclass
74
+ class StateArtifact:
75
+ """A complete, validated interrogation state."""
76
+
77
+ field: str
78
+ session: int
79
+ date: str # YYYY-MM-DD
80
+ depth: str # one-pass | deep
81
+ trigger_context: str
82
+ lifecycle: str # INITIAL | ITERATIVE | FINAL
83
+ prior_interrogation: int | None
84
+ prior_date: str | None
85
+ prior_keys: list[PriorKeyEvaluation]
86
+ takes: list[Take]
87
+ collisions: list[Collision]
88
+ keys: list[KeyDraft]
89
+ surprise: str | None
90
+ synthesis: Synthesis
91
+ evidence_summary: str = ""
92
+ interrogation: int = 1
93
+
94
+ def __post_init__(self) -> None:
95
+ if self.session < 1:
96
+ raise ArtifactError("Session number must be >= 1.")
97
+ if self.lifecycle not in SESSION_LIFECYCLES:
98
+ raise ArtifactError(f"Bad lifecycle: {self.lifecycle!r}.")
99
+ pairs = required_collision_pairs([t.id for t in self.takes])
100
+ got = [c.pair for c in self.collisions]
101
+ if got != pairs:
102
+ raise ArtifactError(
103
+ f"Collision pairs must be exactly {pairs}, got {got}."
104
+ )
105
+ validate_keys(self.keys)
106
+
107
+ @property
108
+ def slug(self) -> str:
109
+ return slugify_field(self.field)
110
+
111
+ @property
112
+ def filename(self) -> str:
113
+ return f"interrogation-state-{self.slug}.md"
114
+
115
+
116
+ def slugify_field(name: str) -> str:
117
+ slug = name.lower()
118
+ slug = re.sub(r"['\"]", "", slug)
119
+ slug = re.sub(r"[^a-z0-9]+", "-", slug)
120
+ slug = slug.strip("-")[:64].strip("-")
121
+ return slug or "field"
122
+
123
+
124
+ def render_frontmatter(a: StateArtifact) -> str:
125
+ return (
126
+ "---\n"
127
+ "artifact: interrogation-state\n"
128
+ f"field: {a.slug}\n"
129
+ f"interrogation: {a.interrogation}\n"
130
+ f"date: {a.date}\n"
131
+ f"depth: {a.depth}\n"
132
+ f"trigger_context: {a.trigger_context}\n"
133
+ "status: complete\n"
134
+ f"lifecycle: {a.lifecycle}\n"
135
+ f"protocol: {PROTOCOL_NAME}/{PROTOCOL_VERSION}\n"
136
+ "persistence: external\n"
137
+ "canonical: true\n"
138
+ "---"
139
+ )
140
+
141
+
142
+ def render(a: StateArtifact) -> str:
143
+ """Render the full canonical Markdown artifact."""
144
+ a.__post_init__() # re-validate before emission
145
+ fm = render_frontmatter(a)
146
+
147
+ prior_lines = [
148
+ f"- Prior interrogation: {a.prior_interrogation if a.prior_interrogation else 'Not supplied'}",
149
+ f"- Prior date: {a.prior_date or '—'}",
150
+ ]
151
+ if a.prior_keys:
152
+ prior_lines.append("- Key dispositions:")
153
+ for k in a.prior_keys:
154
+ prior_lines.append(f" - {k.status}: {k.statement} — {k.note}")
155
+ else:
156
+ prior_lines.append("- Key dispositions: none (session 1)")
157
+
158
+ take_lines = []
159
+ for t in a.takes:
160
+ take_lines.append(f"### Take {t.id} — {t.title}\n\n{t.argument}")
161
+
162
+ collision_lines = []
163
+ for c in a.collisions:
164
+ collision_lines.append(
165
+ f"### Pair {c.pair}\n\n"
166
+ f"- Contradiction: {c.contradiction}\n"
167
+ f"- Premise that must break: {c.premise_failure}\n"
168
+ f"- Discriminator: {c.discriminator}"
169
+ )
170
+
171
+ key_lines = []
172
+ for i, k in enumerate(a.keys, 1):
173
+ key_lines.append(
174
+ f"### Key {i}\n\n"
175
+ f"- Statement: {k.statement}\n"
176
+ f"- Classification: {k.classification}\n"
177
+ f"- Evidence: {k.evidence}\n"
178
+ f"- Confidence: {k.confidence}\n"
179
+ f"- Structural vulnerability: {k.vulnerability}\n"
180
+ f"- Falsifier: {k.falsifier}"
181
+ )
182
+
183
+ body = "\n\n".join([
184
+ fm,
185
+ f"# Interrogation State: {a.field}",
186
+ "## Interrogation Metadata\n"
187
+ f"- Date: {a.date}\n"
188
+ f"- Field: {a.field}\n"
189
+ f"- Depth: {a.depth}\n"
190
+ f"- Interrogation: {a.interrogation}\n"
191
+ f"- Trigger context: {a.trigger_context}\n"
192
+ f"- Protocol: {PROTOCOL_NAME}/{PROTOCOL_VERSION}\n"
193
+ f"- Lifecycle: {a.lifecycle}",
194
+ "## Prior State\n" + "\n".join(prior_lines),
195
+ f"## 1. Identify\n- Field: {a.field}\n- Trigger: {a.trigger_context}",
196
+ f"## 2. Decompose\n{a.evidence_summary or '(evidence logged with the interrogation)'}",
197
+ "## 3. Diverge\n\n" + "\n\n".join(take_lines),
198
+ "## 4. Collide\n\n" + "\n\n".join(collision_lines),
199
+ "## 5. Refine\n\n" + "\n\n".join(key_lines),
200
+ "## 6. Surprise\n\n" + (a.surprise or "No material Surprise identified."),
201
+ "## 7. Synthesize\n"
202
+ f"- Established: {a.synthesis.established_ground}\n"
203
+ f"- Surviving model: {a.synthesis.surviving_model}\n"
204
+ f"- Remaining uncertainties: {a.synthesis.remaining_uncertainties}\n"
205
+ f"- Primary next target: {a.synthesis.primary_next_target}",
206
+ "## State Status\n"
207
+ f"- Interrogation: Complete\n"
208
+ f"- Lifecycle: {a.lifecycle}\n"
209
+ f"- State: Canonical\n"
210
+ f"- Persistence: External\n"
211
+ "- External save: performed by the caller, not the engine",
212
+ ])
213
+ return body + "\n"
214
+
215
+
216
+ def check_sections(markdown: str) -> list[str]:
217
+ """Return any canonical schema sections missing from rendered Markdown."""
218
+ return [s for s in SCHEMA_SECTIONS if f"## {s}" not in markdown]
219
+
220
+
221
+ @dataclass
222
+ class PriorState:
223
+ """A loaded prior artifact for stress-testing in session 2+."""
224
+
225
+ artifact: StateArtifact
226
+
227
+ def dispositions(self) -> list[PriorKeyEvaluation]:
228
+ return self.artifact.prior_keys
229
+
230
+ def next_session(self) -> int:
231
+ return self.artifact.interrogation + 1
232
+
233
+
234
+ def load_prior(markdown: str) -> dict:
235
+ """Extract the prior Keys and session number from a saved artifact.
236
+
237
+ Returns {'interrogation': int, 'keys': [statements...]}. The engine uses
238
+ this to stress-test each prior Key and assign dispositions — it never
239
+ inherits a prior conclusion as established.
240
+ """
241
+ keys: list[str] = []
242
+ for m in re.finditer(r"- Statement:\s*(.+)", markdown):
243
+ keys.append(m.group(1).strip())
244
+ session = 1
245
+ m = re.search(r"^interrogation:\s*(\d+)", markdown, re.M)
246
+ if m:
247
+ session = int(m.group(1))
248
+ return {"interrogation": session, "keys": keys}