From bfc0c5e35a3898a86e90c3109eff639089d02edb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:33:16 +0900 Subject: [PATCH 1/8] feat(enterprise): add semantic issue trust boundary --- .../scoring/enterprise_issue/semantic.py | 565 ++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 python/fast_mlsirm/scoring/enterprise_issue/semantic.py diff --git a/python/fast_mlsirm/scoring/enterprise_issue/semantic.py b/python/fast_mlsirm/scoring/enterprise_issue/semantic.py new file mode 100644 index 000000000..b5c0c7360 --- /dev/null +++ b/python/fast_mlsirm/scoring/enterprise_issue/semantic.py @@ -0,0 +1,565 @@ +"""Provider-neutral semantic issue extraction trust boundary. + +The boundary accepts semantic issue proposals from offline fixtures, human tools, +or future provider adapters, then replays exact source and span provenance before +returning fresh canonical :class:`AtomicIssueRecord` values. It performs no +scoring, calibration, ranking, utility, causal, or sentiment arithmetic and +retains no raw source text. +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from itertools import pairwise +from types import MappingProxyType +from typing import Any, Protocol, runtime_checkable + +from .._contract_safety import bounded_values +from .._validation import assessment_error +from .contracts import ( + MAX_ENTERPRISE_ISSUE_EVIDENCE, + MAX_ENTERPRISE_ISSUE_SOURCES, + AtomicIssueRecord, + CounterevidenceRecord, + EnterpriseAssertionKind, + EnterpriseSourceRecord, + EvidenceSpanRecord, +) + +MAX_ENTERPRISE_ATOMIC_ISSUES = 128 + + +@runtime_checkable +class EnterpriseAtomicIssueExtractor(Protocol): + """Provider-neutral semantic issue extractor protocol.""" + + def extract( + self, + source_records: tuple[EnterpriseSourceRecord, ...], + source_text_by_id: Mapping[str, str], + ) -> tuple[AtomicIssueRecord, ...]: + """Propose atomic issues for exact verified source revisions.""" + ... + + +def _canonical_source_record(item: Any, path: str) -> EnterpriseSourceRecord: + """Reconstruct one exact source record through canonical validation.""" + if type(item) is not EnterpriseSourceRecord: + raise assessment_error( + "invalid_enterprise_source_records", + path, + "source records must contain exact EnterpriseSourceRecord values", + ) + try: + return EnterpriseSourceRecord( + source_id=item.source_id, + source_family_id=item.source_family_id, + source_content_fingerprint=item.source_content_fingerprint, + source_character_count=item.source_character_count, + metadata=item.metadata, + schema_version=item.schema_version, + ) + except Exception: # noqa: BLE001 - untrusted source packet boundary + raise assessment_error( + "invalid_enterprise_source_records", + path, + "source record is not canonical", + ) from None + + +def _canonical_source_records( + values: Iterable[EnterpriseSourceRecord], +) -> tuple[EnterpriseSourceRecord, ...]: + """Return unique fresh source records in deterministic content order.""" + raw = bounded_values( + values, + "source_records", + minimum=1, + maximum=MAX_ENTERPRISE_ISSUE_SOURCES, + ) + records = tuple( + _canonical_source_record(item, f"$.source_records[{index}]") + for index, item in enumerate(raw) + ) + source_ids = tuple(item.source_id for item in records) + if len(set(source_ids)) != len(source_ids): + raise assessment_error( + "duplicate_enterprise_source_id", + "$.source_records", + "source identifiers must be unique", + ) + fingerprints = tuple(item.source_record_fingerprint for item in records) + if len(set(fingerprints)) != len(fingerprints): + raise assessment_error( + "duplicate_enterprise_source_record", + "$.source_records", + "source record fingerprints must be unique", + ) + if len({item.schema_version for item in records}) != 1: + raise assessment_error( + "mixed_enterprise_source_schema", + "$.source_records", + "source records must use one schema version", + ) + return tuple(sorted(records, key=lambda item: item.source_record_fingerprint)) + + +def _verified_source_texts( + records: tuple[EnterpriseSourceRecord, ...], + values: Mapping[str, str], +) -> dict[str, str]: + """Replay exact transient source text against every declared source record.""" + if type(values) is not dict: + raise assessment_error( + "invalid_enterprise_source_texts", + "$.source_text_by_id", + "source_text_by_id must be an exact dictionary", + ) + if any(type(key) is not str for key in values): + raise assessment_error( + "invalid_enterprise_source_texts", + "$.source_text_by_id", + "source text keys must be built-in strings", + ) + expected_ids = {item.source_id for item in records} + if set(values) != expected_ids: + raise assessment_error( + "enterprise_source_text_key_mismatch", + "$.source_text_by_id", + "source text keys must exactly match declared source identifiers", + ) + verified: dict[str, str] = {} + for record in records: + text = values[record.source_id] + if type(text) is not str: + raise assessment_error( + "invalid_enterprise_source_text", + f"$.source_text_by_id.{record.source_id}", + "source text must be a built-in string", + ) + try: + encoded = text.encode("utf-8") + except UnicodeEncodeError: + raise assessment_error( + "invalid_enterprise_source_text", + f"$.source_text_by_id.{record.source_id}", + "source text must be valid UTF-8", + ) from None + if len(text) != record.source_character_count: + raise assessment_error( + "enterprise_source_character_count_mismatch", + f"$.source_text_by_id.{record.source_id}", + "source text character count does not match its source record", + ) + if hashlib.sha256(encoded).hexdigest() != record.source_content_fingerprint: + raise assessment_error( + "enterprise_source_content_fingerprint_mismatch", + f"$.source_text_by_id.{record.source_id}", + "source text fingerprint does not match its source record", + ) + verified[record.source_id] = text + return verified + + +def _canonical_span( + item: Any, + *, + source_by_id: Mapping[str, EnterpriseSourceRecord], + source_text_by_id: Mapping[str, str], + schema_version: str, + path: str, +) -> EvidenceSpanRecord: + """Rebuild one exact provider span after source and UTF-8 slice replay.""" + if type(item) is not EvidenceSpanRecord: + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "semantic issue spans must be exact EvidenceSpanRecord values", + ) + if type(item.source_id) is not str or item.source_id not in source_by_id: + raise assessment_error( + "semantic_issue_source_mismatch", + path, + "semantic issue span names an undeclared source identifier", + ) + source_record = source_by_id[item.source_id] + if ( + type(item.source_record_fingerprint) is not str + or item.source_record_fingerprint != source_record.source_record_fingerprint + ): + raise assessment_error( + "semantic_issue_source_mismatch", + path, + "semantic issue span source identity does not match the source packet", + ) + if ( + isinstance(item.start_offset, bool) + or type(item.start_offset) is not int + or isinstance(item.end_offset, bool) + or type(item.end_offset) is not int + ): + raise assessment_error( + "invalid_semantic_issue_span", + path, + "semantic issue span offsets must be built-in integers", + ) + text = source_text_by_id[item.source_id] + if not 0 <= item.start_offset < item.end_offset <= len(text): + raise assessment_error( + "semantic_issue_span_out_of_bounds", + path, + "semantic issue span exceeds verified source text", + ) + span_fingerprint = hashlib.sha256( + text[item.start_offset : item.end_offset].encode("utf-8") + ).hexdigest() + if ( + type(item.span_content_fingerprint) is not str + or item.span_content_fingerprint != span_fingerprint + ): + raise assessment_error( + "semantic_issue_span_fingerprint_mismatch", + path, + "semantic issue span fingerprint does not match verified source text", + ) + try: + return EvidenceSpanRecord( + source_id=source_record.source_id, + source_record_fingerprint=source_record.source_record_fingerprint, + span_id=item.span_id, + span_content_fingerprint=span_fingerprint, + assertion_kind=item.assertion_kind, + start_offset=item.start_offset, + end_offset=item.end_offset, + metadata=item.metadata, + schema_version=schema_version, + ) + except Exception: # noqa: BLE001 - untrusted provider output boundary + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "semantic issue span is not canonical", + ) from None + + +def _canonical_counterevidence( + item: Any, + *, + issue_content_fingerprint: str, + source_by_id: Mapping[str, EnterpriseSourceRecord], + source_text_by_id: Mapping[str, str], + schema_version: str, + path: str, +) -> CounterevidenceRecord: + """Rebuild one exact counterevidence record and its nested span.""" + if type(item) is not CounterevidenceRecord: + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "counterevidence must contain exact CounterevidenceRecord values", + ) + span = _canonical_span( + item.evidence_span, + source_by_id=source_by_id, + source_text_by_id=source_text_by_id, + schema_version=schema_version, + path=f"{path}.evidence_span", + ) + if span.assertion_kind is not EnterpriseAssertionKind.COUNTEREVIDENCE: + raise assessment_error( + "invalid_semantic_issue_counterevidence", + path, + "counterevidence must preserve the counterevidence assertion kind", + ) + try: + return CounterevidenceRecord( + counterevidence_id=item.counterevidence_id, + issue_content_fingerprint=issue_content_fingerprint, + evidence_span=span, + metadata=item.metadata, + schema_version=schema_version, + ) + except Exception: # noqa: BLE001 - untrusted provider output boundary + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "counterevidence record is not canonical", + ) from None + + +def _reject_overlapping_issue_spans( + evidence_spans: tuple[EvidenceSpanRecord, ...], + counterevidence_records: tuple[CounterevidenceRecord, ...], + path: str, +) -> None: + """Reject duplicated source occurrence coverage within one proposed issue.""" + spans = evidence_spans + tuple( + record.evidence_span for record in counterevidence_records + ) + ordered = sorted( + spans, + key=lambda item: ( + item.source_record_fingerprint, + item.start_offset, + item.end_offset, + item.evidence_span_fingerprint, + ), + ) + for previous, current in pairwise(ordered): + if ( + previous.source_record_fingerprint == current.source_record_fingerprint + and current.start_offset < previous.end_offset + ): + raise assessment_error( + "overlapping_semantic_issue_evidence", + path, + "semantic issue evidence spans must not overlap", + ) + + +def _canonical_issue( + item: Any, + *, + source_by_id: Mapping[str, EnterpriseSourceRecord], + source_by_fingerprint: Mapping[str, EnterpriseSourceRecord], + source_text_by_id: Mapping[str, str], + schema_version: str, + path: str, +) -> AtomicIssueRecord: + """Reconstruct one fresh atomic issue and all nested provenance.""" + if type(item) is not AtomicIssueRecord: + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "extractor output must contain exact AtomicIssueRecord values", + ) + if type(item.issue_content_fingerprint) is not str: + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "issue content fingerprint must be canonical", + ) + raw_sources = bounded_values( + item.source_record_fingerprints, + "source_record_fingerprints", + minimum=1, + maximum=MAX_ENTERPRISE_ISSUE_SOURCES, + path=f"{path}.source_record_fingerprints", + ) + if any(type(value) is not str or value not in source_by_fingerprint for value in raw_sources): + raise assessment_error( + "semantic_issue_source_mismatch", + f"{path}.source_record_fingerprints", + "semantic issue source revisions must belong to the verified packet", + ) + evidence_raw = bounded_values( + item.evidence_spans, + "evidence_spans", + minimum=0, + maximum=MAX_ENTERPRISE_ISSUE_EVIDENCE, + path=f"{path}.evidence_spans", + ) + evidence = tuple( + _canonical_span( + value, + source_by_id=source_by_id, + source_text_by_id=source_text_by_id, + schema_version=schema_version, + path=f"{path}.evidence_spans[{index}]", + ) + for index, value in enumerate(evidence_raw) + ) + counter_raw = bounded_values( + item.counterevidence_records, + "counterevidence_records", + minimum=0, + maximum=MAX_ENTERPRISE_ISSUE_EVIDENCE, + path=f"{path}.counterevidence_records", + ) + counterevidence = tuple( + _canonical_counterevidence( + value, + issue_content_fingerprint=item.issue_content_fingerprint, + source_by_id=source_by_id, + source_text_by_id=source_text_by_id, + schema_version=schema_version, + path=f"{path}.counterevidence_records[{index}]", + ) + for index, value in enumerate(counter_raw) + ) + _reject_overlapping_issue_spans(evidence, counterevidence, path) + try: + return AtomicIssueRecord( + issue_id=item.issue_id, + issue_family_id=item.issue_family_id, + issue_content_fingerprint=item.issue_content_fingerprint, + source_record_fingerprints=tuple(raw_sources), + evidence_spans=evidence, + counterevidence_records=counterevidence, + metadata=item.metadata, + schema_version=schema_version, + ) + except Exception: # noqa: BLE001 - untrusted provider output boundary + raise assessment_error( + "invalid_semantic_issue_extractor_output", + path, + "semantic issue record is not canonical", + ) from None + + +def _validated_extractor_output( + values: Any, + *, + source_records: tuple[EnterpriseSourceRecord, ...], + source_text_by_id: Mapping[str, str], +) -> tuple[AtomicIssueRecord, ...]: + """Return bounded unique fresh atomic issues in deterministic order.""" + if type(values) is not tuple: + raise assessment_error( + "invalid_semantic_issue_extractor_output", + "$.extractor_output", + "extractor output must be a tuple of AtomicIssueRecord values", + ) + if len(values) > MAX_ENTERPRISE_ATOMIC_ISSUES: + raise assessment_error( + "enterprise_atomic_issue_limit", + "$.extractor_output", + "extractor output exceeds the bounded atomic issue limit", + ) + source_by_id = {item.source_id: item for item in source_records} + source_by_fingerprint = { + item.source_record_fingerprint: item for item in source_records + } + schema_version = source_records[0].schema_version + issues = tuple( + _canonical_issue( + item, + source_by_id=source_by_id, + source_by_fingerprint=source_by_fingerprint, + source_text_by_id=source_text_by_id, + schema_version=schema_version, + path=f"$.extractor_output[{index}]", + ) + for index, item in enumerate(values) + ) + issue_fingerprints = tuple(item.atomic_issue_fingerprint for item in issues) + if len(set(issue_fingerprints)) != len(issue_fingerprints): + raise assessment_error( + "duplicate_enterprise_atomic_issue", + "$.extractor_output", + "atomic issue records must be unique", + ) + issue_ids = tuple(item.issue_id for item in issues) + if len(set(issue_ids)) != len(issue_ids): + raise assessment_error( + "duplicate_enterprise_issue_id", + "$.extractor_output", + "atomic issue identifiers must be unique", + ) + family_revisions = tuple( + (item.issue_family_id, item.issue_content_fingerprint) for item in issues + ) + if len(set(family_revisions)) != len(family_revisions): + raise assessment_error( + "duplicate_enterprise_issue_revision", + "$.extractor_output", + "issue family and content revision pairs must be unique", + ) + return tuple( + sorted( + issues, + key=lambda item: ( + item.issue_id, + item.issue_family_id, + item.issue_content_fingerprint, + item.atomic_issue_fingerprint, + ), + ) + ) + + +@dataclass(frozen=True) +class StaticEnterpriseIssueExtractor: + """Deterministic offline fixture adapter, not a semantic language model.""" + + issues: tuple[AtomicIssueRecord, ...] + + def __post_init__(self) -> None: + """Store a bounded exact tuple for deterministic tests and integration.""" + if type(self.issues) is not tuple: + raise assessment_error( + "invalid_static_enterprise_issues", + "$.issues", + "issues must be an exact tuple", + ) + if len(self.issues) > MAX_ENTERPRISE_ATOMIC_ISSUES: + raise assessment_error( + "enterprise_atomic_issue_limit", + "$.issues", + "issues exceed the bounded atomic issue limit", + ) + if any(type(item) is not AtomicIssueRecord for item in self.issues): + raise assessment_error( + "invalid_static_enterprise_issues", + "$.issues", + "issues must contain exact AtomicIssueRecord values", + ) + + def extract( + self, + source_records: tuple[EnterpriseSourceRecord, ...], + source_text_by_id: Mapping[str, str], + ) -> tuple[AtomicIssueRecord, ...]: + """Return declared fixtures without inspecting transient source text.""" + del source_records, source_text_by_id + return tuple(self.issues) + + +def extract_enterprise_atomic_issues( + source_records: Iterable[EnterpriseSourceRecord], + source_text_by_id: Mapping[str, str], + *, + extractor: EnterpriseAtomicIssueExtractor, +) -> tuple[AtomicIssueRecord, ...]: + """Extract and replay-validate semantic issues for exact source revisions. + + Acceptance proves only that the provider proposed canonical issue and evidence + structures whose source spans replay against the supplied source packet. It + does not establish issue truth, completeness, materiality, extraction + accuracy, fairness, construct validity, causal relevance, intervention value, + or readiness for consequential automation. + """ + records = _canonical_source_records(source_records) + texts = _verified_source_texts(records, source_text_by_id) + if not isinstance(extractor, EnterpriseAtomicIssueExtractor): + raise assessment_error( + "invalid_enterprise_atomic_issue_extractor", + "$.extractor", + "extractor must implement EnterpriseAtomicIssueExtractor", + ) + try: + values = extractor.extract( + records, + MappingProxyType(dict(texts)), + ) + except Exception: # noqa: BLE001 - untrusted callback boundary + raise assessment_error( + "enterprise_atomic_issue_extractor_failure", + "$.extractor", + "extractor failed before returning validated atomic issues", + ) from None + return _validated_extractor_output( + values, + source_records=records, + source_text_by_id=texts, + ) + + +__all__ = [ + "MAX_ENTERPRISE_ATOMIC_ISSUES", + "EnterpriseAtomicIssueExtractor", + "StaticEnterpriseIssueExtractor", + "extract_enterprise_atomic_issues", +] From 754cb6c9647648b0464c2db9e4b566bf4d1e8f66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:33:53 +0900 Subject: [PATCH 2/8] feat(enterprise): export semantic issue boundary --- .../scoring/enterprise_issue/__init__.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/python/fast_mlsirm/scoring/enterprise_issue/__init__.py b/python/fast_mlsirm/scoring/enterprise_issue/__init__.py index cb679c8f2..de395e5b0 100644 --- a/python/fast_mlsirm/scoring/enterprise_issue/__init__.py +++ b/python/fast_mlsirm/scoring/enterprise_issue/__init__.py @@ -16,15 +16,11 @@ from .contracts import MAX_ENTERPRISE_STAKEHOLDERS as MAX_ENTERPRISE_STAKEHOLDERS from .contracts import StakeholderPerspective as StakeholderPerspective from .explicit_values import DEFAULT_CURRENCY_CODES as DEFAULT_CURRENCY_CODES -from .explicit_values import ( - MAX_CURRENCY_CODES as MAX_CURRENCY_CODES, -) +from .explicit_values import MAX_CURRENCY_CODES as MAX_CURRENCY_CODES from .explicit_values import ( MAX_CUSTOMER_IDENTIFIER_CHARACTERS as MAX_CUSTOMER_IDENTIFIER_CHARACTERS, ) -from .explicit_values import ( - MAX_EXPLICIT_VALUE_RECORDS as MAX_EXPLICIT_VALUE_RECORDS, -) +from .explicit_values import MAX_EXPLICIT_VALUE_RECORDS as MAX_EXPLICIT_VALUE_RECORDS from .explicit_values import ( DeterministicExplicitValueParser as DeterministicExplicitValueParser, ) @@ -45,11 +41,24 @@ from .request import ( enterprise_issue_evidence_references as enterprise_issue_evidence_references, ) +from .semantic import ( + MAX_ENTERPRISE_ATOMIC_ISSUES as MAX_ENTERPRISE_ATOMIC_ISSUES, +) +from .semantic import ( + EnterpriseAtomicIssueExtractor as EnterpriseAtomicIssueExtractor, +) +from .semantic import ( + StaticEnterpriseIssueExtractor as StaticEnterpriseIssueExtractor, +) +from .semantic import ( + extract_enterprise_atomic_issues as extract_enterprise_atomic_issues, +) __all__ = [ "DEFAULT_CURRENCY_CODES", "MAX_CURRENCY_CODES", "MAX_CUSTOMER_IDENTIFIER_CHARACTERS", + "MAX_ENTERPRISE_ATOMIC_ISSUES", "MAX_ENTERPRISE_ISSUE_EVIDENCE", "MAX_ENTERPRISE_ISSUE_SOURCES", "MAX_ENTERPRISE_SOURCE_CHARACTERS", @@ -60,14 +69,17 @@ "CounterevidenceRecord", "DeterministicExplicitValueParser", "EnterpriseAssertionKind", + "EnterpriseAtomicIssueExtractor", "EnterpriseExplicitValueParser", "EnterpriseSourceRecord", "EvidenceSpanRecord", "ExplicitValueKind", "ExplicitValueRecord", "StakeholderPerspective", + "StaticEnterpriseIssueExtractor", "build_enterprise_issue_score_observation", "build_enterprise_issue_scoring_request", "enterprise_issue_evidence_references", + "extract_enterprise_atomic_issues", "parse_enterprise_explicit_values", ] From 52c6e4061f0f9fb183f21c0e0b06db5d0a017af3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:37:08 +0900 Subject: [PATCH 3/8] test(enterprise): cover semantic issue trust boundary --- .../test_scoring_enterprise_issue_semantic.py | 666 ++++++++++++++++++ 1 file changed, 666 insertions(+) create mode 100644 tests/test_scoring_enterprise_issue_semantic.py diff --git a/tests/test_scoring_enterprise_issue_semantic.py b/tests/test_scoring_enterprise_issue_semantic.py new file mode 100644 index 000000000..1cf3bdd2a --- /dev/null +++ b/tests/test_scoring_enterprise_issue_semantic.py @@ -0,0 +1,666 @@ +"""Deterministic tests for the enterprise semantic issue trust boundary.""" + +from __future__ import annotations + +import hashlib +from types import MappingProxyType +from typing import Any + +import pytest + +import fast_mlsirm.scoring.enterprise_issue as enterprise +from fast_mlsirm.scoring import AssessmentSpecError +from fast_mlsirm.scoring.enterprise_issue import ( + MAX_ENTERPRISE_ATOMIC_ISSUES, + MAX_ENTERPRISE_ISSUE_SOURCES, + AtomicIssueRecord, + CounterevidenceRecord, + EnterpriseAssertionKind, + EnterpriseAtomicIssueExtractor, + EnterpriseSourceRecord, + EvidenceSpanRecord, + StaticEnterpriseIssueExtractor, + extract_enterprise_atomic_issues, +) +from fast_mlsirm.scoring.enterprise_issue import semantic as semantic_module + +SOURCE_TEXT = ( + "Reported delivery missed. " + "Analyst infers capacity risk. " + "Resolution log contradicts delay. " + "Scope remains ambiguous. " + "Operations prefers staged rollout." +) +SECOND_TEXT = "Independent audit confirms the recorded delivery timestamp." +ISSUE_CONTENT_FP = hashlib.sha256(b"semantic-delivery-issue").hexdigest() +SECOND_ISSUE_CONTENT_FP = hashlib.sha256(b"semantic-audit-issue").hexdigest() + + +def _source( + text: str = SOURCE_TEXT, + *, + source_id: str = "primary_source", + source_family_id: str = "customer_feedback", +) -> EnterpriseSourceRecord: + """Return one exact source revision for transient fixture text.""" + return EnterpriseSourceRecord( + source_id=source_id, + source_family_id=source_family_id, + source_content_fingerprint=hashlib.sha256(text.encode("utf-8")).hexdigest(), + source_character_count=len(text), + metadata={"source_channel": "offline_fixture"}, + ) + + +def _span( + source: EnterpriseSourceRecord, + text: str, + snippet: str, + kind: EnterpriseAssertionKind, + span_id: str, + *, + start_offset: int | None = None, +) -> EvidenceSpanRecord: + """Return one exact UTF-8 replayable semantic evidence span.""" + start = text.index(snippet) if start_offset is None else start_offset + end = start + len(snippet) + return EvidenceSpanRecord( + source_id=source.source_id, + source_record_fingerprint=source.source_record_fingerprint, + span_id=span_id, + span_content_fingerprint=hashlib.sha256( + text[start:end].encode("utf-8") + ).hexdigest(), + assertion_kind=kind, + start_offset=start, + end_offset=end, + metadata={"extractor_family": "offline_fixture"}, + ) + + +def _issue( + source: EnterpriseSourceRecord | None = None, + text: str = SOURCE_TEXT, + *, + issue_id: str = "delivery_capacity_risk", + issue_family_id: str = "service_delivery_risk", + issue_content_fingerprint: str = ISSUE_CONTENT_FP, + metadata: dict[str, Any] | None = None, +) -> AtomicIssueRecord: + """Return one issue preserving all five epistemic assertion kinds.""" + source_record = _source(text) if source is None else source + direct = _span( + source_record, + text, + "Reported delivery missed", + EnterpriseAssertionKind.DIRECT_FACT, + "reported_delivery_fact", + ) + inference = _span( + source_record, + text, + "Analyst infers capacity risk", + EnterpriseAssertionKind.SUPPORTED_INFERENCE, + "capacity_risk_inference", + ) + counter_span = _span( + source_record, + text, + "Resolution log contradicts delay", + EnterpriseAssertionKind.COUNTEREVIDENCE, + "resolution_counterevidence", + ) + ambiguity = _span( + source_record, + text, + "Scope remains ambiguous", + EnterpriseAssertionKind.UNRESOLVED_AMBIGUITY, + "scope_ambiguity_record", + ) + judgment = _span( + source_record, + text, + "Operations prefers staged rollout", + EnterpriseAssertionKind.STAKEHOLDER_VALUE_JUDGMENT, + "operations_value_judgment", + ) + counter = CounterevidenceRecord( + counterevidence_id="resolution_log_record", + issue_content_fingerprint=issue_content_fingerprint, + evidence_span=counter_span, + metadata={"verification_state": "source_verified"}, + ) + return AtomicIssueRecord( + issue_id=issue_id, + issue_family_id=issue_family_id, + issue_content_fingerprint=issue_content_fingerprint, + source_record_fingerprints=(source_record.source_record_fingerprint,), + evidence_spans=(direct, inference, ambiguity, judgment), + counterevidence_records=(counter,), + metadata={"review_state": "human_required"} if metadata is None else metadata, + ) + + +def _second_issue(source: EnterpriseSourceRecord, text: str) -> AtomicIssueRecord: + """Return a second independent issue for deterministic ordering tests.""" + span = _span( + source, + text, + "Independent audit confirms the recorded delivery timestamp", + EnterpriseAssertionKind.DIRECT_FACT, + "audit_timestamp_fact", + ) + return AtomicIssueRecord( + issue_id="audit_timestamp_record", + issue_family_id="audit_provenance_risk", + issue_content_fingerprint=SECOND_ISSUE_CONTENT_FP, + source_record_fingerprints=(source.source_record_fingerprint,), + evidence_spans=(span,), + counterevidence_records=(), + metadata={"review_state": "human_required"}, + ) + + +def _extract( + issues: tuple[AtomicIssueRecord, ...] | None = None, + *, + sources: tuple[EnterpriseSourceRecord, ...] | None = None, + texts: dict[str, str] | None = None, +): + """Extract declared fixture issues through the public trust boundary.""" + source = _source() + resolved_sources = (source,) if sources is None else sources + resolved_texts = {source.source_id: SOURCE_TEXT} if texts is None else texts + resolved_issues = (_issue(source),) if issues is None else issues + return extract_enterprise_atomic_issues( + resolved_sources, + resolved_texts, + extractor=StaticEnterpriseIssueExtractor(resolved_issues), + ) + + +def _assert_error(code: str, callback) -> AssessmentSpecError: + """Assert one stable redacted semantic boundary error code.""" + with pytest.raises(AssessmentSpecError) as captured: + callback() + assert captured.value.code == code + return captured.value + + +def test_public_semantic_surface_is_explicit_and_documented() -> None: + """The enterprise namespace exports the provider-neutral trust boundary.""" + expected = { + "MAX_ENTERPRISE_ATOMIC_ISSUES", + "EnterpriseAtomicIssueExtractor", + "StaticEnterpriseIssueExtractor", + "extract_enterprise_atomic_issues", + } + assert expected.issubset(set(enterprise.__all__)) + assert extract_enterprise_atomic_issues.__doc__ + assert StaticEnterpriseIssueExtractor.__doc__ + assert isinstance(StaticEnterpriseIssueExtractor(()), EnterpriseAtomicIssueExtractor) + + +def test_fixture_output_is_fresh_canonical_private_and_epistemically_distinct() -> None: + """All five assertion kinds survive fresh canonical reconstruction.""" + source = _source() + provider_issue = _issue(source) + (result,) = _extract((provider_issue,), sources=(source,)) + + assert result is not provider_issue + assert all( + returned is not supplied + for returned, supplied in zip(result.evidence_spans, provider_issue.evidence_spans) + ) + assert result.counterevidence_records[0] is not provider_issue.counterevidence_records[0] + assert result.counterevidence_records[0].evidence_span is not ( + provider_issue.counterevidence_records[0].evidence_span + ) + kinds = {value.assertion_kind for value in result.evidence_spans} + kinds.add(result.counterevidence_records[0].evidence_span.assertion_kind) + assert kinds == set(EnterpriseAssertionKind) + payload = result.to_dict() + assert "source_text" not in repr(payload) + assert "Reported delivery" not in repr(payload) + assert result.issue_id == provider_issue.issue_id + assert result.issue_content_fingerprint == provider_issue.issue_content_fingerprint + + +def test_source_and_issue_input_order_are_not_hidden_features() -> None: + """Source packet and provider output order do not affect canonical output.""" + primary = _source() + secondary = _source( + SECOND_TEXT, + source_id="secondary_source", + source_family_id="audit_record", + ) + primary_issue = _issue(primary) + secondary_issue = _second_issue(secondary, SECOND_TEXT) + texts = {primary.source_id: SOURCE_TEXT, secondary.source_id: SECOND_TEXT} + + first = extract_enterprise_atomic_issues( + (primary, secondary), + texts, + extractor=StaticEnterpriseIssueExtractor((primary_issue, secondary_issue)), + ) + second = extract_enterprise_atomic_issues( + (secondary, primary), + dict(reversed(tuple(texts.items()))), + extractor=StaticEnterpriseIssueExtractor((secondary_issue, primary_issue)), + ) + + assert tuple(item.atomic_issue_fingerprint for item in first) == tuple( + item.atomic_issue_fingerprint for item in second + ) + assert tuple(item.issue_id for item in first) == tuple( + sorted((primary_issue.issue_id, secondary_issue.issue_id)) + ) + + +def test_empty_fixture_and_sentiment_only_text_create_no_issue() -> None: + """The offline fixture is not an NLP heuristic or sentiment model.""" + text = "Wonderful service and a very positive customer sentiment." + source = _source(text) + assert extract_enterprise_atomic_issues( + (source,), + {source.source_id: text}, + extractor=StaticEnterpriseIssueExtractor(()), + ) == () + + +class _RaisingExtractor: + """Extractor that leaks a malicious provider message if not redacted.""" + + def extract(self, source_records, source_text_by_id): + del source_records, source_text_by_id + raise AssessmentSpecError( + "provider_secret", + "$.provider", + "SECRET source text and provider details", + ) + + +class _ListExtractor: + """Extractor returning the wrong collection type.""" + + def extract(self, source_records, source_text_by_id): + del source_records, source_text_by_id + return [] + + +class _ValueExtractor: + """Extractor returning one caller-controlled tuple.""" + + def __init__(self, values: tuple[Any, ...]) -> None: + self.values = values + + def extract(self, source_records, source_text_by_id): + del source_records, source_text_by_id + return self.values + + +def test_invalid_protocol_and_provider_failures_are_redacted() -> None: + """Provider objects and all callback exceptions fail through fixed messages.""" + source = _source() + _assert_error( + "invalid_enterprise_atomic_issue_extractor", + lambda: extract_enterprise_atomic_issues( + (source,), {source.source_id: SOURCE_TEXT}, extractor=object() + ), + ) + error = _assert_error( + "enterprise_atomic_issue_extractor_failure", + lambda: extract_enterprise_atomic_issues( + (source,), {source.source_id: SOURCE_TEXT}, extractor=_RaisingExtractor() + ), + ) + assert "SECRET" not in str(error) + _assert_error( + "invalid_semantic_issue_extractor_output", + lambda: extract_enterprise_atomic_issues( + (source,), {source.source_id: SOURCE_TEXT}, extractor=_ListExtractor() + ), + ) + + +def test_source_collection_is_bounded_exact_unique_and_canonical( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Untrusted source collections stop at bounds and reject ambiguous identity.""" + source = _source() + _assert_error( + "invalid_source_records", + lambda: extract_enterprise_atomic_issues( + (), {}, extractor=StaticEnterpriseIssueExtractor(()) + ), + ) + _assert_error( + "invalid_enterprise_source_records", + lambda: extract_enterprise_atomic_issues( + (object(),), {}, extractor=StaticEnterpriseIssueExtractor(()) + ), + ) + duplicate_id = _source("Different text", source_id=source.source_id) + _assert_error( + "duplicate_enterprise_source_id", + lambda: extract_enterprise_atomic_issues( + (source, duplicate_id), + {source.source_id: SOURCE_TEXT}, + extractor=StaticEnterpriseIssueExtractor(()), + ), + ) + + consumed: list[int] = [] + + def prolific_sources(): + for index in range(MAX_ENTERPRISE_ISSUE_SOURCES + 2): + consumed.append(index) + yield source + + _assert_error( + "invalid_source_records", + lambda: extract_enterprise_atomic_issues( + prolific_sources(), + {source.source_id: SOURCE_TEXT}, + extractor=StaticEnterpriseIssueExtractor(()), + ), + ) + assert len(consumed) == MAX_ENTERPRISE_ISSUE_SOURCES + 1 + + first = _source() + second = _source( + SECOND_TEXT, + source_id="secondary_source", + source_family_id="audit_record", + ) + original = semantic_module._canonical_source_record + monkeypatch.setattr( + semantic_module, + "_canonical_source_record", + lambda item, path: original(item, path), + ) + monkeypatch.setattr( + EnterpriseSourceRecord, + "source_record_fingerprint", + property(lambda self: "a" * 64), + ) + _assert_error( + "duplicate_enterprise_source_record", + lambda: semantic_module._canonical_source_records((first, second)), + ) + + +def test_mutated_source_record_and_mixed_schema_fail_closed( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Canonical reconstruction and packet schema consistency are mandatory.""" + source = _source() + object.__setattr__(source, "metadata", {"source_text": "secret"}) + _assert_error( + "invalid_enterprise_source_records", + lambda: extract_enterprise_atomic_issues( + (source,), {source.source_id: SOURCE_TEXT}, extractor=StaticEnterpriseIssueExtractor(()) + ), + ) + + first = _source() + second = _source( + SECOND_TEXT, + source_id="secondary_source", + source_family_id="audit_record", + ) + object.__setattr__(second, "schema_version", "9.9") + monkeypatch.setattr( + semantic_module, + "_canonical_source_record", + lambda item, path: item, + ) + _assert_error( + "mixed_enterprise_source_schema", + lambda: semantic_module._canonical_source_records((first, second)), + ) + + +@pytest.mark.parametrize( + ("texts", "code"), + ( + (MappingProxyType({"primary_source": SOURCE_TEXT}), "invalid_enterprise_source_texts"), + ({1: SOURCE_TEXT}, "invalid_enterprise_source_texts"), + ({}, "enterprise_source_text_key_mismatch"), + ({"primary_source": SOURCE_TEXT, "extra_source": "x"}, "enterprise_source_text_key_mismatch"), + ({"primary_source": 1}, "invalid_enterprise_source_text"), + ({"primary_source": "short"}, "enterprise_source_character_count_mismatch"), + ( + {"primary_source": "X" + SOURCE_TEXT[1:]}, + "enterprise_source_content_fingerprint_mismatch", + ), + ({"primary_source": "\ud800"}, "invalid_enterprise_source_text"), + ), +) +def test_source_text_packet_replay_fails_closed(texts: Any, code: str) -> None: + """Mappings, UTF-8, counts, keys, and content hashes are replay-verified.""" + source = _source() + _assert_error( + code, + lambda: extract_enterprise_atomic_issues( + (source,), texts, extractor=StaticEnterpriseIssueExtractor(()) + ), + ) + + +def test_output_collection_type_limit_and_exact_issue_type() -> None: + """Provider output is an exact bounded tuple of exact atomic issues.""" + source = _source() + issue = _issue(source) + _assert_error( + "enterprise_atomic_issue_limit", + lambda: extract_enterprise_atomic_issues( + (source,), + {source.source_id: SOURCE_TEXT}, + extractor=_ValueExtractor((issue,) * (MAX_ENTERPRISE_ATOMIC_ISSUES + 1)), + ), + ) + _assert_error( + "invalid_semantic_issue_extractor_output", + lambda: extract_enterprise_atomic_issues( + (source,), + {source.source_id: SOURCE_TEXT}, + extractor=_ValueExtractor((object(),)), + ), + ) + + +@pytest.mark.parametrize( + "change", + ("source_id", "source_fingerprint", "start_bool", "start_text", "end_bool", "end_text", "bounds", "span_type", "span_fingerprint_type", "span_fingerprint"), +) +def test_nested_span_replay_rejects_malformed_provider_records(change: str) -> None: + """Exact nested types, source identity, offsets, and UTF-8 slices are enforced.""" + source = _source() + issue = _issue(source) + span = issue.evidence_spans[0] + if change == "source_id": + object.__setattr__(span, "source_id", "unknown_source") + elif change == "source_fingerprint": + object.__setattr__(span, "source_record_fingerprint", "a" * 64) + elif change == "start_bool": + object.__setattr__(span, "start_offset", True) + elif change == "start_text": + object.__setattr__(span, "start_offset", "0") + elif change == "end_bool": + object.__setattr__(span, "end_offset", True) + elif change == "end_text": + object.__setattr__(span, "end_offset", "1") + elif change == "bounds": + object.__setattr__(span, "end_offset", len(SOURCE_TEXT) + 1) + elif change == "span_type": + class SpanSubclass(EvidenceSpanRecord): + """Untrusted nested subclass.""" + + replacement = SpanSubclass( + source_id=span.source_id, + source_record_fingerprint=span.source_record_fingerprint, + span_id=span.span_id, + span_content_fingerprint=span.span_content_fingerprint, + assertion_kind=span.assertion_kind, + start_offset=span.start_offset, + end_offset=span.end_offset, + metadata=span.metadata, + ) + object.__setattr__(issue, "evidence_spans", (replacement,) + issue.evidence_spans[1:]) + elif change == "span_fingerprint_type": + object.__setattr__(span, "span_content_fingerprint", 1) + else: + object.__setattr__(span, "span_content_fingerprint", "b" * 64) + + expected = { + "source_id": "semantic_issue_source_mismatch", + "source_fingerprint": "semantic_issue_source_mismatch", + "start_bool": "invalid_semantic_issue_span", + "start_text": "invalid_semantic_issue_span", + "end_bool": "invalid_semantic_issue_span", + "end_text": "invalid_semantic_issue_span", + "bounds": "semantic_issue_span_out_of_bounds", + "span_type": "invalid_semantic_issue_extractor_output", + "span_fingerprint_type": "semantic_issue_span_fingerprint_mismatch", + "span_fingerprint": "semantic_issue_span_fingerprint_mismatch", + }[change] + _assert_error( + expected, + lambda: extract_enterprise_atomic_issues( + (source,), + {source.source_id: SOURCE_TEXT}, + extractor=_ValueExtractor((issue,)), + ), + ) + + +def test_malformed_issue_and_counterevidence_records_fail_closed() -> None: + """Issue identity, source binding, counter kind, and metadata are reconstructed.""" + source = _source() + + issue = _issue(source) + object.__setattr__(issue, "issue_content_fingerprint", 1) + _assert_error( + "invalid_semantic_issue_extractor_output", + lambda: _extract_with(source, issue), + ) + + issue = _issue(source) + object.__setattr__(issue, "source_record_fingerprints", ("a" * 64,)) + _assert_error("semantic_issue_source_mismatch", lambda: _extract_with(source, issue)) + + issue = _issue(source) + object.__setattr__(issue, "metadata", {"source_text": "secret"}) + _assert_error( + "invalid_semantic_issue_extractor_output", + lambda: _extract_with(source, issue), + ) + + issue = _issue(source) + counter = issue.counterevidence_records[0] + object.__setattr__(counter.evidence_span, "assertion_kind", EnterpriseAssertionKind.DIRECT_FACT) + _assert_error( + "invalid_semantic_issue_counterevidence", + lambda: _extract_with(source, issue), + ) + + issue = _issue(source) + object.__setattr__(issue, "counterevidence_records", (object(),)) + _assert_error( + "invalid_semantic_issue_extractor_output", + lambda: _extract_with(source, issue), + ) + + +def _extract_with(source: EnterpriseSourceRecord, issue: AtomicIssueRecord): + """Run one mutated issue through an unrestricted provider fixture.""" + return extract_enterprise_atomic_issues( + (source,), + {source.source_id: SOURCE_TEXT}, + extractor=_ValueExtractor((issue,)), + ) + + +def test_overlapping_and_duplicate_nested_evidence_fail_closed() -> None: + """One occurrence cannot be silently multiplied inside an atomic issue.""" + source = _source() + issue = _issue(source) + direct = issue.evidence_spans[0] + overlapping = EvidenceSpanRecord( + source_id=direct.source_id, + source_record_fingerprint=direct.source_record_fingerprint, + span_id="overlapping_fact_record", + span_content_fingerprint=hashlib.sha256( + SOURCE_TEXT[direct.start_offset + 1 : direct.end_offset].encode("utf-8") + ).hexdigest(), + assertion_kind=EnterpriseAssertionKind.DIRECT_FACT, + start_offset=direct.start_offset + 1, + end_offset=direct.end_offset, + metadata={"extractor_family": "offline_fixture"}, + ) + object.__setattr__(issue, "evidence_spans", issue.evidence_spans + (overlapping,)) + _assert_error( + "overlapping_semantic_issue_evidence", + lambda: _extract_with(source, issue), + ) + + issue = _issue(source) + object.__setattr__(issue, "evidence_spans", issue.evidence_spans + (issue.evidence_spans[0],)) + _assert_error( + "overlapping_semantic_issue_evidence", + lambda: _extract_with(source, issue), + ) + + +def test_duplicate_issue_identity_dimensions_fail_closed() -> None: + """Content, logical identifier, and family-revision duplicates are distinct gates.""" + source = _source() + first = _issue(source) + _assert_error( + "duplicate_enterprise_atomic_issue", + lambda: _extract((first, first), sources=(source,)), + ) + + same_id = _issue( + source, + issue_id=first.issue_id, + issue_content_fingerprint=SECOND_ISSUE_CONTENT_FP, + metadata={"review_state": "second_revision"}, + ) + _assert_error( + "duplicate_enterprise_issue_id", + lambda: _extract((first, same_id), sources=(source,)), + ) + + same_revision = _issue( + source, + issue_id="alternate_issue_record", + issue_family_id=first.issue_family_id, + issue_content_fingerprint=first.issue_content_fingerprint, + metadata={"review_state": "alternate_record"}, + ) + _assert_error( + "duplicate_enterprise_issue_revision", + lambda: _extract((first, same_revision), sources=(source,)), + ) + + +def test_static_fixture_constructor_is_exact_and_bounded() -> None: + """The offline fixture itself cannot hide prolific or subclass output.""" + source = _source() + issue = _issue(source) + _assert_error( + "invalid_static_enterprise_issues", + lambda: StaticEnterpriseIssueExtractor([issue]), + ) + _assert_error( + "enterprise_atomic_issue_limit", + lambda: StaticEnterpriseIssueExtractor( + (issue,) * (MAX_ENTERPRISE_ATOMIC_ISSUES + 1) + ), + ) + _assert_error( + "invalid_static_enterprise_issues", + lambda: StaticEnterpriseIssueExtractor((object(),)), + ) From 6e1b1181f64ee507dee6f97f88b3c95ac0e50b5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:37:43 +0900 Subject: [PATCH 4/8] docs(plan): define semantic issue trust boundary --- ...8-05-enterprise-semantic-issue-boundary.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-05-enterprise-semantic-issue-boundary.md diff --git a/docs/superpowers/plans/2026-08-05-enterprise-semantic-issue-boundary.md b/docs/superpowers/plans/2026-08-05-enterprise-semantic-issue-boundary.md new file mode 100644 index 000000000..5b18acbb4 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-enterprise-semantic-issue-boundary.md @@ -0,0 +1,118 @@ +# Enterprise semantic-issue provider boundary + +## Objective + +Advance issue #404 with the smallest provider-neutral semantic extraction slice +that can be reviewed independently after the deterministic explicit-value parser +and governed criterion-observation adapter. The slice lets offline fixtures, +human analysts, and future external engines propose the existing +`AtomicIssueRecord` contract without introducing a second issue, evidence, +observation, scoring, or engine schema. + +## Architectural boundary + +The implementation belongs under `fast_mlsirm.scoring.enterprise_issue` and +reuses: + +- `EnterpriseSourceRecord` as the exact source-revision contract; +- `EvidenceSpanRecord` and `EnterpriseAssertionKind` for every semantic assertion; +- `CounterevidenceRecord` for counterevidence separation; +- `AtomicIssueRecord` as the only accepted semantic issue output; and +- the shared scoring request, observation, result, and engine contracts for later + criterion-level scoring. + +The package must not import a provider SDK. Python may validate, replay, +canonicalize, redact, and marshal this boundary. This slice adds no likelihood, +gradient, Hessian, optimization, scoring, ranking, or utility arithmetic. + +## Public API + +Implement: + +- runtime-checkable `EnterpriseAtomicIssueExtractor`; +- `extract_enterprise_atomic_issues()` as the fail-closed public entry point; +- bounded `MAX_ENTERPRISE_ATOMIC_ISSUES`; and +- `StaticEnterpriseIssueExtractor` as an offline fixture and integration adapter, + not a semantic language model or production default. + +No competing extraction-request or semantic-assertion record is permitted where +the existing source, span, counterevidence, and atomic-issue contracts suffice. + +## Trust-boundary requirements + +The public entry point must: + +1. consume source records through bounded iteration; +2. require exact `EnterpriseSourceRecord` values, unique source IDs, unique source + record fingerprints, one schema version, and deterministic ordering; +3. require an exact dictionary whose keys equal the declared source IDs; +4. replay transient source text against its Python character count, UTF-8 + encodability, and SHA-256 content fingerprint; +5. invoke only an object satisfying the provider-neutral protocol; +6. redact every provider exception, including structured domain exceptions; +7. accept only a bounded exact tuple of exact `AtomicIssueRecord` values; +8. reconstruct fresh canonical issue, evidence-span, and counterevidence objects; +9. bind every referenced source fingerprint and source ID to the same supplied + source revision; +10. verify every code-point span offset and SHA-256 fingerprint over the exact + UTF-8 slice; +11. preserve all five assertion kinds and wrapped counterevidence; +12. reject duplicate issue fingerprints, duplicate issue IDs, duplicate + family/revision pairs, overlapping or duplicated nested spans, malformed + nested records, and oversized outputs; +13. return deterministic content order; and +14. retain no raw source text or clear-text semantic issue text in public records, + exceptions, metadata, logs, or serialized output. + +Use stable structured error codes and JSON paths. Never reflect untrusted values +in error messages. + +## Scientific and product limits + +An accepted record proves only that an extractor proposed one canonical +issue/evidence structure whose spans replay against exact source revisions. It +does not prove that the issue is true, complete, material, probable, causally +related to an outcome, fair, construct-valid, or suitable for intervention +automation. An inference is not converted into a fact, and counterevidence is not +assumed weaker than supporting evidence. + +The fixture extractor must not be described as a semantic model. Human validation +and held-out provider evaluation remain prerequisites for product claims. + +## Validation + +Tests must provide complete statement and branch coverage and demonstrate: + +- deterministic output under source-record and issue reordering; +- exact source replay, Unicode code-point offsets, and UTF-8 span fingerprints; +- survival of all five assertion kinds without epistemic collapse; +- fresh canonical reconstruction and rejection of mutated or subclassed nested + records; +- provider exception redaction and privacy preservation; +- missing, extra, duplicated, mismatched, oversized, or unexpectedly prolific + source/output collections failing before unbounded consumption; +- source ID/fingerprint pair consistency; +- changed text, offsets, or span bytes invalidating replay; +- duplicate and overlapping nested evidence rejection; +- sentiment-only source text creating no issue in the deterministic fixture path; + and +- stable package exports and serialized shapes. + +Run Ruff, focused and repository tests, branch coverage, changelog parity, +packaging, Security Scan, SAST, and exact-head acceptance gates. Every public +object requires a complete docstring. + +## Documentation and changelog + +Update `docs/enterprise_issue_evidence_contracts.md` with provider trust, +source/span replay, fixture limitations, exception redaction, and conservative +interpretation limits. Add an authoritative changelog fragment and render +`CHANGELOG.md`. Do not bump a version or publish a release for this isolated +issue #404 slice. + +## Review discipline + +Keep the pull request draft until its exact current head has no unresolved valid +human, CodeRabbit, security, Dependabot, or automated feedback and all required +gates pass. Do not leave temporary workflows, triggers, credentials, raw source +fixtures, or generated artifacts in the final tree. From 6330780533544c3da0cbdbb4cce6d38fe3a2d71f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:38:39 +0900 Subject: [PATCH 5/8] docs(enterprise): document semantic issue trust boundary --- docs/enterprise_issue_evidence_contracts.md | 72 +++++++++++++++------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/docs/enterprise_issue_evidence_contracts.md b/docs/enterprise_issue_evidence_contracts.md index d0dceab4c..893aec070 100644 --- a/docs/enterprise_issue_evidence_contracts.md +++ b/docs/enterprise_issue_evidence_contracts.md @@ -3,17 +3,18 @@ `fast_mlsirm.scoring.enterprise_issue` provides the first provider-neutral domain boundary for issue #404. The module stores content identities, exact source-span offsets, epistemic roles, stakeholder perspectives, candidate-intervention -provenance, deterministic explicit values, criterion-level request provenance, -and governed criterion observations without retaining raw enterprise text. +provenance, deterministic explicit values, semantic issue proposals, +criterion-level request provenance, and governed criterion observations without +retaining raw enterprise text. ## Contract boundary -The initial slice deliberately separates five assertion kinds: +The domain deliberately separates five assertion kinds: - directly stated facts; - supported inferences; - counterevidence; -- unresolved ambiguities; +- unresolved ambiguities; and - stakeholder value judgments. `EvidenceSpanRecord.to_evidence_reference()` compiles each span into the existing @@ -22,11 +23,11 @@ supporting evidence, counterevidence maps to counter evidence, and ambiguities o value judgments map to contextual evidence. This mapping does not convert an inference into a fact or a preference into a materiality estimate. -The canonical records contain no source text, complaint text, lead notes, -customer names, or proposed-action text. Callers retain those values in an -authorized source system and pass SHA-256 content fingerprints plus offsets. -Sensitive metadata fields already prohibited by the shared scoring contract are -rejected here as well. +Canonical records contain no source text, complaint text, lead notes, customer +names, or proposed-action text. Callers retain those values in an authorized +source system and pass SHA-256 content fingerprints plus offsets. Sensitive +metadata fields already prohibited by the shared scoring contract are rejected +here as well. ## Deterministic explicit-value parser @@ -40,7 +41,7 @@ that are already explicit in authorized source text. Its first grammar recognize - positive recurrence counts per day, week, month, quarter, or year; and - customer or account identifiers introduced by an explicit identifier label. -The parser verifies the transient text against the exact +The parser verifies transient text against the exact `EnterpriseSourceRecord.source_content_fingerprint` and Python string character count before extraction. Match offsets are Python Unicode-code-point indices, which are appropriate for replaying slices of the same Python `str`; they are not @@ -73,12 +74,45 @@ probable, decision-relevant, or causally related to an outcome. The parser is deliberately not a semantic issue extractor. It performs no sentiment analysis, inference, scoring, calibration, ranking, utility arithmetic, -causal estimation, or queue routing. Semantic assertions remain behind a separate -provider-neutral, human-validated boundary. Custom parser output is bounded, +causal estimation, or queue routing. Custom parser output is bounded, canonicalized, and rebound to the exact verified source revision and span bytes before it can cross the public API. Arbitrary provider exceptions are redacted. Explicit-value caller metadata is restricted to the declared offset unit. +## Semantic issue extraction boundary + +`extract_enterprise_atomic_issues()` is the provider-neutral trust boundary for +semantic issue proposals. It accepts exact source records, transient source text, +and an `EnterpriseAtomicIssueExtractor`, then returns only fresh canonical +`AtomicIssueRecord` values. The package imports no provider SDK and provides no +default production semantic model. + +Before a provider runs, the boundary consumes source records with a fixed cap, +reconstructs exact source contracts, rejects duplicate source identities, requires +an exact source-text dictionary, and replays every Python character count and +SHA-256 fingerprint over valid UTF-8. Providers receive deterministic source +record order and a read-only transient text mapping. Every provider exception, +including package-domain exceptions, is replaced with a fixed redacted boundary +error. + +Provider output must be an exact bounded tuple of exact `AtomicIssueRecord` +values. Each issue, evidence span, and counterevidence record is reconstructed as +a fresh canonical instance. Every source ID and source-record fingerprint must +name the same verified packet revision, and every span must replay its exact +code-point offsets and SHA-256 fingerprint over the corresponding UTF-8 slice. +Nested subclasses, malformed or mutated records, overlapping spans, duplicate +issues, duplicate logical issue IDs, and duplicate family/content revisions fail +closed. Returned issues use deterministic content order and retain no raw text. + +`StaticEnterpriseIssueExtractor` is an offline fixture and integration adapter. It +returns only caller-declared issue records and performs no NLP, sentiment +analysis, inference, generation, ranking, or automatic issue discovery. Adding +sentiment-only text cannot create an issue in this path. Acceptance proves only +that a provider proposed one replayable canonical structure, not that the issue +is true, complete, material, probable, fair, construct-valid, or operationally +useful. Human validation and held-out provider evaluation remain prerequisites +for product claims. + ## Replay and provenance An `AtomicIssueRecord` binds one issue-content revision to declared source-record @@ -116,7 +150,7 @@ and auditability but is not itself evidence that a claim is accurate or strong. Stakeholder perspectives and candidate interventions must name the exact issue content revision. Perspective evidence must also reference a source revision -already declared by the issue. Caller metadata cannot overwrite the managed +already declared by the issue. Caller metadata cannot overwrite managed enterprise provenance fields. This compiler performs deterministic validation and marshaling only. Existing @@ -137,7 +171,7 @@ the issue declares counterevidence, the observation must also retain at least on counterevidence reference rather than silently omitting contradictory evidence. An engine that cannot satisfy those conditions must abstain with a stable reason code. Abstention may retain no evidence and is not converted into a low score. -Caller confidence metadata cannot overwrite the managed enterprise provenance. +Caller confidence metadata cannot overwrite managed enterprise provenance. The adapter does not calculate a rating or confidence value. Score categories, criterion coverage, terminal-state semantics, engine identity, assessment and @@ -149,8 +183,8 @@ is accurate, reliable, fair, valid, calibrated, or decision-ready. These contracts improve traceability and replay resistance; they do not establish construct validity, evidence truth, causal identification, model fairness, or -high-stakes deployment readiness. The request and observation compilers perform -no sentiment analysis, latent measurement, calibration, comparative ranking, +high-stakes deployment readiness. The extraction, request, and observation +adapters perform no latent measurement, calibration, comparative ranking, expected utility, value-of-information, intervention-effect, or queue-routing arithmetic. @@ -164,9 +198,9 @@ legal rights, or material consequences are in dispute. A candidate intervention is a caller-supplied hypothesis, not evidence of an identified causal effect. ISO/IEC 42001:2023 remains published as Edition 1. ISO 8601-1:2019 and ISO -4217:2015 are the published standards used to describe the accepted date and +4217:2015 are the published standards used to describe accepted date and currency-code forms; later amendments, maintenance updates, or replacement -editions must be evaluated before changing the parser grammar or allowlists. NIST +editions must be evaluated before changing parser grammar or allowlists. NIST reports that AI RMF 1.0 is under revision as of August 2026, so this module cites the current published framework without assuming that its terminology or profiles are frozen. @@ -195,7 +229,7 @@ risk management framework (AI RMF 1.0)* (NIST AI 100-1). https://doi.org/10.6028/NIST.AI.100-1 Python Software Foundation. (2025). *Python 3.13 standard library: `datetime`, -`decimal`, `hashlib`, `re`, and `typing`*. https://docs.python.org/3.13/ +`hashlib`, `types`, and `typing`*. https://docs.python.org/3.13/ The Unicode Consortium. (2025). *Unicode text segmentation* (Unicode Standard Annex No. 29, Revision 47). https://www.unicode.org/reports/tr29/ From eedb5e5c41ade7c97d8fdc640b0d0ea2132b8b0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:38:54 +0900 Subject: [PATCH 6/8] docs(changelog): add semantic issue boundary fragment --- .../enterprise-semantic-issue-boundary.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/changelog.d/enterprise-semantic-issue-boundary.md diff --git a/docs/changelog.d/enterprise-semantic-issue-boundary.md b/docs/changelog.d/enterprise-semantic-issue-boundary.md new file mode 100644 index 000000000..30ed584f3 --- /dev/null +++ b/docs/changelog.d/enterprise-semantic-issue-boundary.md @@ -0,0 +1,18 @@ +# Enterprise semantic issue provider boundary + +## Added + +- Added runtime-checkable `EnterpriseAtomicIssueExtractor` and + `extract_enterprise_atomic_issues` as a provider-neutral, provider-SDK-free trust + boundary that returns the existing canonical `AtomicIssueRecord` contract. +- Added bounded exact source-packet replay, UTF-8 and Python code-point span + verification, fresh nested issue/evidence/counterevidence reconstruction, + deterministic ordering, duplicate and overlap rejection, and redacted provider + failures without retaining raw enterprise text. +- Added `StaticEnterpriseIssueExtractor` as an offline fixture and integration + adapter that performs no NLP, sentiment analysis, issue discovery, scoring, + ranking, utility, or causal arithmetic. +- Added deterministic order-invariance, all-assertion-kind preservation, + malicious provider, source mutation, span replay, subclass, privacy, prolific + collection, duplicate identity, overlap, and complete statement/branch coverage + tests for the next issue #404 workflow slice. From a2e45f3e39c6a3c00ae542d50847cf7ed15ebf66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 11:39:04 +0900 Subject: [PATCH 7/8] ci: render semantic issue changelog --- .../semantic-issue-render-changelog.yml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/semantic-issue-render-changelog.yml diff --git a/.github/workflows/semantic-issue-render-changelog.yml b/.github/workflows/semantic-issue-render-changelog.yml new file mode 100644 index 000000000..4ffdf5c87 --- /dev/null +++ b/.github/workflows/semantic-issue-render-changelog.yml @@ -0,0 +1,37 @@ +name: Render semantic issue changelog + +on: + push: + branches: + - feat/enterprise-semantic-issue-boundary + paths: + - .github/workflows/semantic-issue-render-changelog.yml + +permissions: + contents: write + +concurrency: + group: semantic-issue-render-changelog + cancel-in-progress: false + +jobs: + render: + runs-on: ubuntu-latest + steps: + - name: Checkout exact branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: feat/enterprise-semantic-issue-boundary + - name: Render authoritative fragments + run: | + python scripts/render_changelog_fragments.py --update CHANGELOG.md + python scripts/render_changelog_fragments.py --check CHANGELOG.md + - name: Remove one-shot workflow and commit + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm .github/workflows/semantic-issue-render-changelog.yml + git add CHANGELOG.md + git diff --cached --check + git commit -m "docs(changelog): render semantic issue fragment" + git push origin HEAD:feat/enterprise-semantic-issue-boundary From 4528248e35bfb169ad9458093e9e98c040129013 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:39:14 +0000 Subject: [PATCH 8/8] docs(changelog): render semantic issue fragment --- .../semantic-issue-render-changelog.yml | 37 ------------------- CHANGELOG.md | 17 +++++++++ 2 files changed, 17 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/semantic-issue-render-changelog.yml diff --git a/.github/workflows/semantic-issue-render-changelog.yml b/.github/workflows/semantic-issue-render-changelog.yml deleted file mode 100644 index 4ffdf5c87..000000000 --- a/.github/workflows/semantic-issue-render-changelog.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Render semantic issue changelog - -on: - push: - branches: - - feat/enterprise-semantic-issue-boundary - paths: - - .github/workflows/semantic-issue-render-changelog.yml - -permissions: - contents: write - -concurrency: - group: semantic-issue-render-changelog - cancel-in-progress: false - -jobs: - render: - runs-on: ubuntu-latest - steps: - - name: Checkout exact branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - ref: feat/enterprise-semantic-issue-boundary - - name: Render authoritative fragments - run: | - python scripts/render_changelog_fragments.py --update CHANGELOG.md - python scripts/render_changelog_fragments.py --check CHANGELOG.md - - name: Remove one-shot workflow and commit - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git rm .github/workflows/semantic-issue-render-changelog.yml - git add CHANGELOG.md - git diff --cached --check - git commit -m "docs(changelog): render semantic issue fragment" - git push origin HEAD:feat/enterprise-semantic-issue-boundary diff --git a/CHANGELOG.md b/CHANGELOG.md index 7beb8d31c..6392b3bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,23 @@ source, reserved metadata, sensitive-content, ordering-invariance, and shared contract delegation tests for issue #404. +#### Enterprise semantic issue provider boundary + +- Added runtime-checkable `EnterpriseAtomicIssueExtractor` and + `extract_enterprise_atomic_issues` as a provider-neutral, provider-SDK-free trust + boundary that returns the existing canonical `AtomicIssueRecord` contract. +- Added bounded exact source-packet replay, UTF-8 and Python code-point span + verification, fresh nested issue/evidence/counterevidence reconstruction, + deterministic ordering, duplicate and overlap rejection, and redacted provider + failures without retaining raw enterprise text. +- Added `StaticEnterpriseIssueExtractor` as an offline fixture and integration + adapter that performs no NLP, sentiment analysis, issue discovery, scoring, + ranking, utility, or causal arithmetic. +- Added deterministic order-invariance, all-assertion-kind preservation, + malicious provider, source mutation, span replay, subclass, privacy, prolific + collection, duplicate identity, overlap, and complete statement/branch coverage + tests for the next issue #404 workflow slice. + #### Accessible standalone essay facets-calibration artifacts - Added `render_essay_facets_calibration_report_html`, which replay-verifies one governed `EssayFacetsCalibrationReport` and emits a deterministic, source-text-free, script-free standalone HTML audit artifact.