Spaces:
Sleeping
Sleeping
| """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, | |
| neutralise_dangling_placeholders, | |
| ) | |
| # 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) | |
| # 5. Drop dangling date/temporal clauses whose only value is the placeholder | |
| # (e.g. over-redacted regulation dates in standard safety boilerplate). | |
| text = neutralise_dangling_placeholders(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 | |
| ) | |