File size: 10,006 Bytes
ef85832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
debca42
ef85832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9481c68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef85832
 
 
 
 
bca2667
 
 
 
ef85832
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bca2667
ef85832
 
 
 
 
 
 
 
 
 
 
9481c68
ef85832
 
 
 
 
 
 
 
 
 
 
 
 
9481c68
bca2667
9481c68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bca2667
9481c68
 
 
 
 
 
 
 
bca2667
9481c68
 
 
bca2667
9481c68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bca2667
9481c68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bca2667
9481c68
 
 
 
 
 
 
bca2667
9481c68
 
 
 
 
 
 
bca2667
9481c68
 
 
 
 
 
ef85832
debca42
30ba196
 
 
 
 
ef85832
debca42
30ba196
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
from __future__ import annotations

from pathlib import Path

from replacement_decision import (
    VALID_REVIEW_STATES,
    VALID_SCOPES,
    build_replacement_audit,
    build_replacement_decision,
    matching_occurrence_ids,
)


REPO_ROOT = Path(__file__).resolve().parents[1]
UI_PLAN = REPO_ROOT / "REPLACE_LOGIC_UI_PLAN.md"
THIS_TEST = Path(__file__)

UI_ACTION_TO_STATE = {
    "Vervangen": "accepted",
    "Vervanging aanpassen": "edited",
    "Zichtbaar houden": "ignored",
    "Handmatig gemiste waarde toevoegen": "manual_added",
    "Als context behouden": "preserve_context",
    "Later controleren": "unresolved",
}

UI_SCOPE_TO_HELPER_SCOPE = {
    "Alleen deze plek": "this_occurrence",
    "Alle exact dezelfde waarden": "all_exact",
    "Alle genormaliseerde gelijke waarden": "all_normalized",
}

ALLOWED_VIEW_ONLY_SESSION_KEYS = {
    "replacement_decision_selected_occurrence_id",
    "replacement_decision_preview_state",
    "replacement_decision_preview_scope",
    "replacement_decision_preview_text",
    "replacement_decision_panel_expanded",
}

FORBIDDEN_MUTATION_TARGETS = {
    "replacement_editor",
    "edited_replacements_df",
    "review table rows",
    "export state",
    "scrub key state",
    "reinsert state",
}


def _ui_plan_text() -> str:
    return UI_PLAN.read_text(encoding="utf-8")


def _contract_text() -> str:
    return _ui_plan_text().replace("`", "").lower()


def test_ui_action_contract_maps_only_to_supported_helper_states():
    assert set(UI_ACTION_TO_STATE.values()).issubset(VALID_REVIEW_STATES)

    for label, state in UI_ACTION_TO_STATE.items():
        decision = build_replacement_decision(
            occurrence_id=f"occ-{state}",
            source_text="SYNTHETIC-WAARDE",
            entity_type="SYNTHETIC_ENTITY",
            display_label=label,
            suggested_replacement="[SYNTHETIC_1]",
            final_replacement="[SYNTHETIC_EDITED]" if state == "edited" else None,
            review_state=state,
            scope="this_occurrence",
        )
        assert decision.review_state == state
        if state in {"ignored", "preserve_context", "unresolved"}:
            assert decision.creates_mapping is False
            assert decision.replacement_value is None
        else:
            assert decision.creates_mapping is True
            assert decision.replacement_value is not None


def test_ui_scope_contract_maps_only_to_supported_helper_scopes_and_matching_rules():
    assert set(UI_SCOPE_TO_HELPER_SCOPE.values()).issubset(VALID_SCOPES)

    occurrences = [
        {"occurrence_id": "one", "source_text": "SYNTHETIC VALUE"},
        {"occurrence_id": "two", "source_text": "SYNTHETIC VALUE"},
        {"occurrence_id": "three", "source_text": "synthetic   value"},
        {"occurrence_id": "four", "source_text": "OTHER SYNTHETIC VALUE"},
    ]

    exact_decision = build_replacement_decision(
        occurrence_id="one",
        source_text="SYNTHETIC VALUE",
        entity_type="SYNTHETIC_ENTITY",
        display_label="Vervangen",
        suggested_replacement="[SYNTHETIC_1]",
        review_state="accepted",
        scope="all_exact",
    )
    normalized_decision = build_replacement_decision(
        occurrence_id="one",
        source_text="SYNTHETIC VALUE",
        entity_type="SYNTHETIC_ENTITY",
        display_label="Vervangen",
        suggested_replacement="[SYNTHETIC_1]",
        review_state="accepted",
        scope="all_normalized",
    )

    assert matching_occurrence_ids(exact_decision, occurrences) == ["one", "two"]
    assert matching_occurrence_ids(normalized_decision, occurrences) == ["one", "two", "three"]


def test_ui_plan_contains_required_labels_states_scopes_and_confirmation_language():
    text = _ui_plan_text()
    lowered = text.lower()

    for label in UI_ACTION_TO_STATE:
        assert label in text
    for state in UI_ACTION_TO_STATE.values():
        assert f"`{state}`" in text
    for label in UI_SCOPE_TO_HELPER_SCOPE:
        assert label in text
    for scope in UI_SCOPE_TO_HELPER_SCOPE.values():
        assert f"`{scope}`" in text

    assert "affected count" in lowered
    assert "stronger confirmation" in lowered
    assert "no fuzzy matching or guessed intent" in lowered
    assert "default scope is `this_occurrence`" in lowered


def test_audit_contract_is_report_only_and_export_readiness_is_advisory():
    decisions = [
        build_replacement_decision(
            occurrence_id="accepted-1",
            source_text="SYNTHETIC-A",
            entity_type="SYNTHETIC_ENTITY",
            display_label="Vervangen",
            suggested_replacement="[SYNTHETIC_A]",
            review_state="accepted",
        ),
        build_replacement_decision(
            occurrence_id="later-1",
            source_text="SYNTHETIC-B",
            entity_type="SYNTHETIC_ENTITY",
            display_label="Later controleren",
            suggested_replacement="[SYNTHETIC_B]",
            review_state="unresolved",
            risk_flags=("synthetic_high_risk",),
        ),
    ]

    audit = build_replacement_audit(decisions)

    assert audit["report_only"] is True
    assert audit["export_blocking"] is False
    assert audit["export_readiness"] == "high_risk_unresolved"
    assert audit["unresolved_items"] == ["later-1"]
    assert audit["mapping_candidates"] == ["accepted-1"]
    assert audit["risk_flags"] == ["synthetic_high_risk"]


def test_ui_contract_plan_preserves_boundaries_and_does_not_approve_ui_implementation():
    text = _contract_text()

    for required in [
        "does not implement ui",
        "audit panel is report-only",
        "must not block export",
        "does not approve export blocking",
        "must not change scrub key behavior",
        "highlight preview remains read-only and non-authoritative",
        "no click-to-mark implementation is approved",
        "this plan does not change",
        "presidio_streamlit.py",
        "serial_review_panel_ui.py",
        "fix_streamlit_nested_expanders.py",
        "review table behavior",
        "export/download behavior",
        "scrub key behavior",
        "reinsert behavior",
        "helper runtime behavior",
        "dependencies",
        "cloud processing",
        "real-data fixtures",
    ]:
        assert required in text


def test_staged_vs_applied_state_contract_is_explicit_and_non_mutating():
    text = _contract_text()

    for required in [
        "staged decision preview only",
        "staged decision state is not applied state",
        "existing review table remains source of truth and fallback",
        "no review table mutation",
        "no replacement mutation",
        "no automatic replacement",
        "not write those decisions back",
        "separate package with explicit coordinator approval",
    ]:
        assert required in text


def test_session_state_contract_allows_only_view_only_keys_and_blocks_mutation_targets():
    text = _ui_plan_text()
    contract = _contract_text()

    assert "Allowed view-only session keys" in text
    assert "temporary UI selection and preview state" in text
    assert "must not be treated as applied replacement state" in text

    for key in ALLOWED_VIEW_ONLY_SESSION_KEYS:
        assert key in text
    for target in FORBIDDEN_MUTATION_TARGETS:
        assert f"no mutation of {target}" in contract


def test_scrub_key_mapping_indicators_are_advisory_only():
    text = _contract_text()
    decisions = [
        build_replacement_decision(
            occurrence_id="mapping-1",
            source_text="SYNTHETIC-A",
            entity_type="SYNTHETIC_ENTITY",
            display_label="Vervangen",
            suggested_replacement="[SYNTHETIC_A]",
            review_state="accepted",
        )
    ]
    audit = build_replacement_audit(decisions)

    assert decisions[0].creates_mapping is True
    assert audit["mapping_candidates"] == ["mapping-1"]
    assert "creates_mapping is advisory only" in text
    assert "does not authorize a scrub key write" in text
    assert "mapping_candidates are advisory only" in text
    assert "do not authorize scrub key persistence" in text
    assert "writing mappings directly from a new ui panel" in text
    assert "scrub key schema" in text


def test_export_download_and_reinsert_boundaries_are_explicit():
    text = _contract_text()

    for required in [
        "export_readiness is advisory only",
        "must not call export/download functions",
        "change export eligibility",
        "disable export buttons",
        "alter download payloads",
        "no export/download calls",
        "no reinsert behavior change",
        "no scrub key writes",
    ]:
        assert required in text


def test_no_fuzzy_matching_no_guessed_intent_and_no_automatic_replacement_contract():
    text = _contract_text()

    assert "no fuzzy matching or guessed intent is allowed" in text
    assert "no automatic replacement" in text
    assert "auto-repairing duplicate placeholders" in text


def test_all_normalized_not_available_as_first_mutating_scope_without_approval():
    text = _contract_text()

    assert "all_normalized" in text
    assert "not available as a first mutating ui scope without separate explicit coordinator approval" in text
    assert "disabled/advisory only" in text


def test_future_ui_requires_separate_explicit_coordinator_approval():
    text = _contract_text()

    assert "only after separate explicit coordinator approval" in text
    assert "do not start implementation automatically" in text
    assert "small staged/read-only companion panel" in text


def test_contract_tests_use_synthetic_values_only():
    rendered = THIS_TEST.read_text(encoding="utf-8") + _ui_plan_text()
    forbidden_real_data_examples = [
        "Jan " + "Jansen",
        "Piet " + "de " + "Vries",
        "123" + "456" + "782",
    ]

    assert "SYNTHETIC" in rendered
    for forbidden in forbidden_real_data_examples:
        assert forbidden not in rendered