fix: recognize corroborated empty Oracle associations
This commit is contained in:
1 parent
4559d5a642
commit
de9eb532f2
9 files changed
+735
-28
No files matched your search
+141
-10
@@ -28,6 +28,8 @@ from integrations.ohip.collect_arr_source import json_bytes, require, strict_jso
|
||||
from integrations.ohip.collect_arr_source import CollectionError
|
||||
|
||||
VERSION = "arr-source-field-review/v1"
|
||||
REANALYSIS_POLICY = "oracle-optional-association/v1"
|
||||
REANALYSIS_VERSION = "arr-source-reanalysis/v1"
|
||||
LIMIT = 100 * 1024 * 1024
|
||||
OPTIONAL = frozenset({"BLOCK_CODE", "RES_COMMENT", "PRODUCTS", "ROOM_CATEGORY_LABEL"})
|
||||
LABELS = {
|
||||
@@ -128,6 +130,12 @@ class DataFieldReviews:
|
||||
state = strict_json(state_raw)
|
||||
require(state["identity_sha256"] == fingerprint(meta) and type(state["revision"]) is int
|
||||
and state["status"] in {"editing", "finalized"}, "data_review_state_context_changed")
|
||||
if "source_reanalysis" in state:
|
||||
reference = state["source_reanalysis"]
|
||||
require(type(reference) is dict and reference.get("parent_original_sha256") == meta["original_sha256"]
|
||||
and reference.get("source_manifest_sha256") == meta["source_manifest_sha256"],
|
||||
"data_review_reanalysis_context_changed")
|
||||
self._reanalyzed_source(directory, original, state)
|
||||
return meta, original, state
|
||||
|
||||
def _publish(self, directory, state):
|
||||
@@ -148,8 +156,74 @@ class DataFieldReviews:
|
||||
value = [value]
|
||||
return {"state": "available", "value": value}
|
||||
|
||||
def _derive(self, original, state):
|
||||
data = copy.deepcopy(original)
|
||||
@staticmethod
|
||||
def _reanalysis_projection(original, reanalyzed, policy_id):
|
||||
require(policy_id == REANALYSIS_POLICY and type(reanalyzed) is dict,
|
||||
"data_review_reanalysis_policy_invalid")
|
||||
expected = copy.deepcopy(original)
|
||||
require(type(reanalyzed.get("records")) is list
|
||||
and len(reanalyzed["records"]) == len(original["records"]),
|
||||
"data_review_reanalysis_records_changed")
|
||||
changes = []
|
||||
allowed = {"BLOCK_CODE": ("missing_reservation_block", ""),
|
||||
"PRODUCTS": ("missing_reservation_packages", [])}
|
||||
for index, (old, new) in enumerate(zip(original["records"], reanalyzed["records"])):
|
||||
require(type(new) is dict and type(new.get("fields")) is dict,
|
||||
"data_review_reanalysis_record_invalid")
|
||||
for field, (reason, empty_value) in allowed.items():
|
||||
before, after = old["fields"][field], new["fields"].get(field)
|
||||
if fingerprint(before) == fingerprint(after):
|
||||
continue
|
||||
empty = {"state": "empty", "value": empty_value}
|
||||
require(before.get("state") == "missing" and before.get("reason") == reason
|
||||
and fingerprint(after) == fingerprint(empty),
|
||||
"data_review_reanalysis_observation_invalid")
|
||||
expected["records"][index]["fields"][field] = copy.deepcopy(empty)
|
||||
changes.append({"item_id": f"{old['source_sequence']}:{field}",
|
||||
"source_sequence": old["source_sequence"], "field": field,
|
||||
"original_observation": copy.deepcopy(before), "reanalyzed_observation": copy.deepcopy(empty)})
|
||||
require(bool(changes), "data_review_reanalysis_no_changes")
|
||||
unresolved = any(observation.get("state") != "available"
|
||||
and not (field in OPTIONAL and observation.get("state") == "empty")
|
||||
for row in expected["records"] for field, observation in row["fields"].items())
|
||||
expected["input_complete"] = not unresolved
|
||||
expected["status"] = "collected_with_gaps" if unresolved else "collected"
|
||||
expected["optional_omission_policy"] = REANALYSIS_POLICY
|
||||
require(fingerprint(expected) == fingerprint(reanalyzed), "data_review_reanalysis_source_changed")
|
||||
return changes
|
||||
|
||||
def _reanalyzed_source(self, directory, original, state):
|
||||
reference = state.get("source_reanalysis")
|
||||
if reference is None:
|
||||
return original, None
|
||||
require(type(reference) is dict and reference.get("policy_id") == REANALYSIS_POLICY,
|
||||
"data_review_reanalysis_reference_invalid")
|
||||
digest, receipt_digest = reference.get("sha256", ""), reference.get("receipt_sha256", "")
|
||||
manifest_digest = reference.get("manifest_sha256", "")
|
||||
require(type(digest) is str and bool(re.fullmatch(r"[0-9a-f]{64}", digest))
|
||||
and type(receipt_digest) is str and bool(re.fullmatch(r"[0-9a-f]{64}", receipt_digest))
|
||||
and type(manifest_digest) is str and bool(re.fullmatch(r"[0-9a-f]{64}", manifest_digest))
|
||||
and reference.get("file") == f"reanalyzed-source-{digest}.json"
|
||||
and reference.get("receipt_file") == f"source-reanalysis-{receipt_digest}.json",
|
||||
"data_review_reanalysis_reference_invalid")
|
||||
payload = protected_read(directory / reference["file"], LIMIT)
|
||||
receipt_raw = protected_read(directory / reference["receipt_file"], LIMIT)
|
||||
require(_hash(payload) == digest and _hash(receipt_raw) == receipt_digest,
|
||||
"data_review_reanalysis_changed")
|
||||
reanalyzed, receipt = strict_json(payload), strict_json(receipt_raw)
|
||||
changes = self._reanalysis_projection(original, reanalyzed, REANALYSIS_POLICY)
|
||||
expected_receipt = {"version": REANALYSIS_VERSION, "policy_id": REANALYSIS_POLICY,
|
||||
"request_id": directory.name, "report_date": original["report_date"], "context": self.context,
|
||||
"parent_original_sha256": reference["parent_original_sha256"],
|
||||
"source_manifest_sha256": reference["source_manifest_sha256"],
|
||||
"reanalyzed_sha256": digest, "reanalysis_manifest_sha256": reference["manifest_sha256"],
|
||||
"changes": changes}
|
||||
require(fingerprint(receipt) == fingerprint(expected_receipt), "data_review_reanalysis_receipt_changed")
|
||||
return reanalyzed, receipt
|
||||
|
||||
def _derive(self, directory, original, state):
|
||||
source, _receipt = self._reanalyzed_source(directory, original, state)
|
||||
data = copy.deepcopy(source)
|
||||
for item_id, decision in state["decisions"].items():
|
||||
sequence, field = item_id.split(":")
|
||||
row = data["records"][int(sequence) - 1]
|
||||
@@ -197,7 +271,7 @@ class DataFieldReviews:
|
||||
return value if observation.get("state") == "available" and isinstance(value, (str, int)) else ""
|
||||
|
||||
def _public(self, directory, meta, original, state):
|
||||
data = self._derive(original, state)
|
||||
data = self._derive(directory, original, state)
|
||||
issues = self._issues(directory, data)
|
||||
keys = set(issues) | set(state["decisions"])
|
||||
items = []
|
||||
@@ -209,6 +283,7 @@ class DataFieldReviews:
|
||||
"field_label": LABELS[field], "confirmation_no": self._display(row, "CONFIRMATION_NO"),
|
||||
"room_no": self._display(row, "DISP_ROOM_NO"), "company_name": self._display(row, "COMPANY_NAME"),
|
||||
"rate_code": self._display(row, "RATE_CODE"), "reason_code": issues.get(item_id, "confirmed"),
|
||||
"source_state": row["fields"][field].get("state"),
|
||||
"can_be_empty": field in OPTIONAL, "value": decision["value"] if decision else None,
|
||||
"confirmed": decision is not None and item_id not in issues})
|
||||
return {"request_id": meta["request_id"], "report_date": meta["report_date"],
|
||||
@@ -245,13 +320,66 @@ class DataFieldReviews:
|
||||
require(fingerprint(actual) == fingerprint(meta), "data_review_prepare_conflict")
|
||||
review = self._public(directory, actual, original, state)
|
||||
# A complete ordinary source needs no human step or derived source.
|
||||
return review if review["total_count"] else None
|
||||
# A reanalysis remains an explicit review until finalized, even if
|
||||
# its validated source interpretation resolves every listed gap.
|
||||
return review if review["total_count"] or "source_reanalysis" in state else None
|
||||
|
||||
def get(self, request_id):
|
||||
directory = self._directory(request_id)
|
||||
with self._lock(directory):
|
||||
return self._public(directory, *self._read(directory))
|
||||
|
||||
def apply_source_reanalysis(self, request_id, payload, manifest_sha256, policy_id, *, expected_revision):
|
||||
"""Attach a validated offline reinterpretation, without changing acquisition or staff decisions.
|
||||
|
||||
This internal maintenance operation has no HTTP route. Its caller must
|
||||
establish the policy's omission evidence from the saved complete capture.
|
||||
The projection here prevents any other source or business-value edits.
|
||||
"""
|
||||
require(policy_id == REANALYSIS_POLICY, "data_review_reanalysis_policy_invalid")
|
||||
require(type(payload) is bytes and len(payload) <= LIMIT,
|
||||
"data_review_reanalysis_payload_invalid")
|
||||
require(type(manifest_sha256) is str and bool(re.fullmatch(r"[0-9a-f]{64}", manifest_sha256)),
|
||||
"data_review_reanalysis_manifest_invalid")
|
||||
reanalyzed = strict_json(payload)
|
||||
directory = self._directory(request_id)
|
||||
with self._lock(directory):
|
||||
meta, original, state = self._read(directory)
|
||||
if state["status"] != "editing" or (directory / "finalize-intent.json").exists():
|
||||
raise _error("FROZEN", "已确认生成,来源解释不能再修改", 409)
|
||||
if type(expected_revision) is not int:
|
||||
self._revision(state, expected_revision)
|
||||
digest = _hash(payload)
|
||||
prior = state.get("source_reanalysis")
|
||||
if prior is not None:
|
||||
if prior["sha256"] == digest and prior["manifest_sha256"] == manifest_sha256:
|
||||
return self._public(directory, meta, original, state)
|
||||
raise _error("CONFLICT", "该来源已完成重新解释,请核对现有记录", 409)
|
||||
self._revision(state, expected_revision)
|
||||
changes = self._reanalysis_projection(original, reanalyzed, policy_id)
|
||||
receipt = {"version": REANALYSIS_VERSION, "policy_id": policy_id,
|
||||
"request_id": request_id, "report_date": meta["report_date"], "context": self.context,
|
||||
"parent_original_sha256": meta["original_sha256"],
|
||||
"source_manifest_sha256": meta["source_manifest_sha256"], "reanalyzed_sha256": digest,
|
||||
"reanalysis_manifest_sha256": manifest_sha256, "changes": changes}
|
||||
receipt_raw = json_bytes(receipt)
|
||||
receipt_digest = _hash(receipt_raw)
|
||||
reference = {"policy_id": policy_id, "file": f"reanalyzed-source-{digest}.json", "sha256": digest,
|
||||
"receipt_file": f"source-reanalysis-{receipt_digest}.json", "receipt_sha256": receipt_digest,
|
||||
"parent_original_sha256": meta["original_sha256"],
|
||||
"source_manifest_sha256": meta["source_manifest_sha256"], "manifest_sha256": manifest_sha256}
|
||||
_write_once(directory / reference["file"], payload)
|
||||
_write_once(directory / reference["receipt_file"], receipt_raw)
|
||||
state["source_reanalysis"] = reference
|
||||
state["revision"] += 1
|
||||
state["events"].append({"revision": state["revision"], "action": "source_reanalysis",
|
||||
"policy_id": policy_id, "parent_original_sha256": meta["original_sha256"],
|
||||
"reanalyzed_sha256": digest, "reanalysis_manifest_sha256": manifest_sha256,
|
||||
"receipt_sha256": receipt_digest, "at": datetime.now(timezone.utc).isoformat()})
|
||||
result = self._public(directory, meta, original, state)
|
||||
self._publish(directory, state)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _revision(state, revision):
|
||||
if type(revision) is not int or revision != state["revision"]:
|
||||
@@ -312,7 +440,7 @@ class DataFieldReviews:
|
||||
raise _error("FROZEN", "已确认生成,字段不能再修改", 409)
|
||||
self._revision(state, revision)
|
||||
actor = self._actor(actor)
|
||||
data = self._derive(original, state)
|
||||
data = self._derive(directory, original, state)
|
||||
if item_id not in self._issues(directory, data) and item_id not in state["decisions"]:
|
||||
raise _error(message="只能完善当前任务列出的异常字段")
|
||||
sequence, field = item_id.split(":")
|
||||
@@ -328,7 +456,7 @@ class DataFieldReviews:
|
||||
self._publish(directory, state)
|
||||
return result
|
||||
|
||||
def _frozen(self, meta, original, state):
|
||||
def _frozen(self, directory, meta, original, state):
|
||||
changes = []
|
||||
for item_id, decision in sorted(state["decisions"].items()):
|
||||
sequence, field = item_id.split(":")
|
||||
@@ -339,7 +467,10 @@ class DataFieldReviews:
|
||||
"context": self.context, "original_sha256": meta["original_sha256"],
|
||||
"source_manifest_sha256": meta["source_manifest_sha256"], "revision": state["revision"],
|
||||
"changes": changes, "events": state["events"]}
|
||||
data = self._derive(original, state)
|
||||
_source, receipt = self._reanalyzed_source(directory, original, state)
|
||||
if receipt is not None:
|
||||
audit["source_reanalysis"] = {**receipt, "receipt_sha256": state["source_reanalysis"]["receipt_sha256"]}
|
||||
data = self._derive(directory, original, state)
|
||||
data["manual_data_review"] = {"manifest": audit, "manifest_sha256": fingerprint(audit)}
|
||||
binding = fingerprint({"source_manifest_sha256": meta["source_manifest_sha256"],
|
||||
"review_manifest_sha256": fingerprint(audit), "data_sha256": _hash(json_bytes(data))})
|
||||
@@ -354,7 +485,7 @@ class DataFieldReviews:
|
||||
return self._public(directory, meta, original, state)
|
||||
self._revision(state, revision)
|
||||
actor = self._actor(actor)
|
||||
if self._issues(directory, self._derive(original, state)):
|
||||
if self._issues(directory, self._derive(directory, original, state)):
|
||||
raise _error("INCOMPLETE", "请先完善并确认所有异常字段", 409)
|
||||
intent = directory / "finalize-intent.json"
|
||||
prior_sha256 = fingerprint(state)
|
||||
@@ -368,14 +499,14 @@ class DataFieldReviews:
|
||||
state["events"].append({"revision": state["revision"], "action": "finalize", "actor": actor,
|
||||
"at": datetime.now(timezone.utc).isoformat()})
|
||||
atomic_json(intent, {"prior_sha256": prior_sha256, "state": state}, replace=False)
|
||||
payload, binding = self._frozen(meta, original, state)
|
||||
payload, binding = self._frozen(directory, meta, original, state)
|
||||
_write_once(directory / "reviewed-source.json", payload)
|
||||
state["derived_sha256"], state["binding_manifest_sha256"] = _hash(payload), binding
|
||||
self._publish(directory, state)
|
||||
return self._public(directory, meta, original, state)
|
||||
|
||||
def _verify_frozen(self, directory, meta, original, state):
|
||||
payload, binding = self._frozen(meta, original, state)
|
||||
payload, binding = self._frozen(directory, meta, original, state)
|
||||
require(_hash(payload) == state["derived_sha256"] and binding == state["binding_manifest_sha256"]
|
||||
and protected_read(directory / "reviewed-source.json", LIMIT) == payload,
|
||||
"data_review_frozen_source_changed")
|
||||
|
||||
Reference in new issue
Block a user