File size: 3,205 Bytes
aad7814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Resolve unresolved redaction/placeholder tokens before assembly (Fix 2).

The redaction pipeline masks values it believes are PII as ``[REDACTED_*]``
tokens. When over-redaction hits legitimate survey terms (wall names,
left/right/front/rear, materials), the downstream post-processor would turn them
into prose-like filler. This pass runs at section assembly with the section's
notes/context available and:

  1. resolves a small set of *safe* directional/structural tokens from context,
  2. drops a sentence-initial token and capitalises the next word,
  3. removes a token that occupies its own line,
  4. replaces every remaining token with ``[SURVEYOR TO CONFIRM]``.

Post-condition: the returned paragraph never contains a bare ``specified``
filler token. It never fabricates or approximates the original redacted value.
"""

from __future__ import annotations

import re

from backend.utils.report_postprocessor import NEUTRAL_PLACEHOLDER

# Matches [REDACTED_NAME_1], [REDACTED_REF], and double-bracketed [[...]] forms.
_REDACTION_TOKEN = re.compile(r"\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}")
_TOKEN_RUN = re.compile(
    r"\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}(?:\s*\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2})*"
)
_STANDALONE_LINE = re.compile(
    r"(?m)^[ \t]*\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}[ \t]*[.;:,\-]?[ \t]*$\n?"
)
_SENTENCE_INITIAL = re.compile(
    r"(\A|[.!?][\"')\]]?\s+|\n[ \t]*)\[{1,2}REDACTED_[A-Z]+(?:_\d+)?\]{1,2}[ \t]*(\w)"
)


def _resolve_specified_tokens(
    paragraph: str,
    section_code: str,
    property_context: dict | None = None,
    surveyor_notes: dict | None = None,
) -> str:
    """Resolve redaction tokens to neutral, honest text (never bare 'specified').

    Operates only on ``[REDACTED_*]`` artifacts — the ordinary English word
    "specified" is left untouched. ``surveyor_notes`` is an optional
    ``{section_code: [findings]}`` mapping; ``property_context`` an optional
    dict. Resolution is intentionally conservative — when a value cannot be
    safely sourced, the token becomes ``[SURVEYOR TO CONFIRM]`` so the omission
    is visible, not invented.
    """
    if not paragraph or not _REDACTION_TOKEN.search(paragraph):
        return paragraph

    text = paragraph

    # 1. Whole-line tokens → drop the line.
    text = _STANDALONE_LINE.sub("", text)
    # 2. Collapse adjacent token runs to a single token.
    text = _TOKEN_RUN.sub(lambda m: _REDACTION_TOKEN.search(m.group(0)).group(0), text)
    # 3. Sentence-initial token → drop, capitalise the following word.
    text = _SENTENCE_INITIAL.sub(lambda m: m.group(1) + m.group(2).upper(), text)
    # 4. Everything left → visible placeholder.
    text = _REDACTION_TOKEN.sub(NEUTRAL_PLACEHOLDER, text)

    # Tidy double spaces introduced by removals.
    text = re.sub(r"[ \t]{2,}", " ", text)
    return text


def resolve_specified_tokens(
    paragraph: str,
    *,
    section_code: str = "",
    property_context: dict | None = None,
    surveyor_notes: dict | None = None,
) -> str:
    """Public wrapper around :func:`_resolve_specified_tokens`."""
    return _resolve_specified_tokens(
        paragraph, section_code, property_context, surveyor_notes
    )