feat: complete pre-ai local completion v3
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
"""Offline-safe, ID-correlated provider batch execution."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from .provider import ProviderTransportError
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BatchRunResult:
|
||||||
|
results_by_work_item_id: dict[str, dict] = field(default_factory=dict)
|
||||||
|
errors: dict[str, str] = field(default_factory=dict)
|
||||||
|
provider_calls: int = 0
|
||||||
|
retries: int = 0
|
||||||
|
usage: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def run_batches(items, provider, batch_size: int, max_retries: int = 2, *, scenario: str | None = None) -> BatchRunResult:
|
||||||
|
if batch_size <= 0:
|
||||||
|
raise ValueError("batch_size must be positive")
|
||||||
|
if max_retries < 0:
|
||||||
|
raise ValueError("max_retries must not be negative")
|
||||||
|
|
||||||
|
result = BatchRunResult()
|
||||||
|
for start in range(0, len(items), batch_size):
|
||||||
|
batch = list(items[start:start + batch_size])
|
||||||
|
expected_ids = [str(item.work_item_id) for item in batch]
|
||||||
|
answers = None
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
|
result.provider_calls += 1
|
||||||
|
try:
|
||||||
|
kwargs = {} if scenario is None else {"scenario": scenario}
|
||||||
|
answers = provider.select_batch(batch, **kwargs)
|
||||||
|
break
|
||||||
|
except ProviderTransportError as error:
|
||||||
|
if attempt == max_retries:
|
||||||
|
for work_item_id in expected_ids:
|
||||||
|
result.errors[work_item_id] = str(error)
|
||||||
|
else:
|
||||||
|
result.retries += 1
|
||||||
|
except Exception as error:
|
||||||
|
for work_item_id in expected_ids:
|
||||||
|
result.errors[work_item_id] = str(error)
|
||||||
|
break
|
||||||
|
if answers is None:
|
||||||
|
continue
|
||||||
|
answer_ids = [str(answer.get("work_item_id")) for answer in answers]
|
||||||
|
if len(answer_ids) != len(expected_ids) or set(answer_ids) != set(expected_ids) or len(set(answer_ids)) != len(answer_ids):
|
||||||
|
error = "provider response did not contain exactly one answer per work_item_id"
|
||||||
|
for work_item_id in expected_ids:
|
||||||
|
result.errors[work_item_id] = error
|
||||||
|
continue
|
||||||
|
result.results_by_work_item_id.update({str(answer["work_item_id"]): answer for answer in answers})
|
||||||
|
|
||||||
|
result.usage = dict(getattr(provider, "usage", {}) or {})
|
||||||
|
return result
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Pure cache and dataset-freshness policies for offline and real evaluations."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
VALID, NEEDS_REVALIDATION, CONFLICT = "VALID", "NEEDS_REVALIDATION", "CONFLICT"
|
||||||
|
|
||||||
|
|
||||||
|
def cache_key(input_fingerprint, candidate_set_hash, model, prompt_version, dataset_versions):
|
||||||
|
"""Return a stable key covering every input that can change an answer."""
|
||||||
|
payload = {
|
||||||
|
"input_fingerprint": str(input_fingerprint),
|
||||||
|
"candidate_set_hash": str(candidate_set_hash),
|
||||||
|
"model": str(model),
|
||||||
|
"prompt_version": str(prompt_version),
|
||||||
|
"dataset_versions": dict(sorted((dataset_versions or {}).items())),
|
||||||
|
}
|
||||||
|
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def classify_knowledge_freshness(record_versions, active_versions):
|
||||||
|
"""Classify versioned knowledge without deleting or rewriting history."""
|
||||||
|
recorded = dict(record_versions or {})
|
||||||
|
active = dict(active_versions or {})
|
||||||
|
if any(key in recorded and key in active and recorded[key] != active[key] for key in recorded):
|
||||||
|
return CONFLICT
|
||||||
|
if recorded != active:
|
||||||
|
return NEEDS_REVALIDATION
|
||||||
|
return VALID
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""Deterministic provider responses for offline pipeline tests."""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from .llm_provider import LlmAnswerError, LlmEvalItem, validate_answer
|
||||||
|
from .provider import ProviderContractError, ProviderTransportError
|
||||||
|
|
||||||
|
|
||||||
|
SCENARIOS = frozenset({
|
||||||
|
"CORRECT", "WRONG", "ABSTAIN", "INVALID_JSON", "MISSING_ITEM",
|
||||||
|
"DUPLICATE_ITEM", "OUT_OF_CANDIDATE", "TIMEOUT", "429", "500",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeterministicFakeSemanticProvider:
|
||||||
|
"""Return repeatable answers without persistence or network access."""
|
||||||
|
|
||||||
|
name = "deterministic-fake"
|
||||||
|
model = "deterministic-fake-v1"
|
||||||
|
prompt_version = "fake-provider-v1"
|
||||||
|
|
||||||
|
def __init__(self, *, shuffle: bool = False, fail_work_item_ids: Iterable[str] = (), failure_scenario: str = "TIMEOUT"):
|
||||||
|
self.shuffle = shuffle
|
||||||
|
self.fail_work_item_ids = {str(item_id) for item_id in fail_work_item_ids}
|
||||||
|
self.failure_scenario = failure_scenario
|
||||||
|
self.calls = 0
|
||||||
|
self.usage = {"input_tokens": 0, "output_tokens": 0, "cached_tokens": 0}
|
||||||
|
|
||||||
|
def select_batch(self, items: list[LlmEvalItem], scenario: str | None = None) -> list[dict]:
|
||||||
|
scenario = scenario or "CORRECT"
|
||||||
|
if scenario not in SCENARIOS:
|
||||||
|
raise ValueError(f"unknown fake provider scenario: {scenario}")
|
||||||
|
self.calls += 1
|
||||||
|
if self.fail_work_item_ids and len(items) == 1 and str(items[0].work_item_id) in self.fail_work_item_ids:
|
||||||
|
self._raise_transport(self.failure_scenario)
|
||||||
|
if scenario in {"TIMEOUT", "429", "500"}:
|
||||||
|
self._raise_transport(scenario)
|
||||||
|
if scenario == "INVALID_JSON":
|
||||||
|
raise LlmAnswerError("fake provider returned invalid JSON")
|
||||||
|
|
||||||
|
answers = [self._answer(item, scenario) for item in items]
|
||||||
|
if scenario == "MISSING_ITEM" and answers:
|
||||||
|
answers.pop()
|
||||||
|
elif scenario == "DUPLICATE_ITEM" and answers:
|
||||||
|
answers[-1] = dict(answers[0])
|
||||||
|
if self.shuffle:
|
||||||
|
answers.reverse()
|
||||||
|
expected_ids = [str(item.work_item_id) for item in items]
|
||||||
|
answer_ids = [str(answer.get("work_item_id")) for answer in answers]
|
||||||
|
if len(answer_ids) != len(expected_ids) or set(answer_ids) != set(expected_ids) or len(set(answer_ids)) != len(answer_ids):
|
||||||
|
raise ProviderContractError("fake provider response did not contain exactly one answer per work_item_id")
|
||||||
|
items_by_id = {str(item.work_item_id): item for item in items}
|
||||||
|
try:
|
||||||
|
return [validate_answer(answer, items_by_id[str(answer["work_item_id"])]) for answer in answers]
|
||||||
|
except (LlmAnswerError, KeyError, TypeError) as error:
|
||||||
|
raise ProviderContractError(str(error)) from error
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _answer(item: LlmEvalItem, scenario: str) -> dict:
|
||||||
|
candidates = [str(candidate[0]) for candidate in item.candidates]
|
||||||
|
selected = None
|
||||||
|
abstain = scenario == "ABSTAIN"
|
||||||
|
if not abstain and candidates:
|
||||||
|
selected = "99999999" if scenario == "OUT_OF_CANDIDATE" else candidates[min(1, len(candidates) - 1) if scenario == "WRONG" else 0]
|
||||||
|
return {
|
||||||
|
"work_item_id": item.work_item_id,
|
||||||
|
"selected_ncm": selected,
|
||||||
|
"confidence": 0.0 if abstain else 0.75,
|
||||||
|
"abstain": abstain or selected is None,
|
||||||
|
"reason_short": f"fake scenario {scenario}",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _raise_transport(scenario: str) -> None:
|
||||||
|
if scenario not in {"TIMEOUT", "429", "500"}:
|
||||||
|
raise ValueError(f"unknown fake transport scenario: {scenario}")
|
||||||
|
raise ProviderTransportError(f"fake provider transport failure: {scenario}")
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Conservative, explainable product identity and conflict classification."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from .fingerprint import normalize_description, token_stems, validate_gtin
|
||||||
|
|
||||||
|
GTIN_CONFLICT = "GTIN_CONFLICT"
|
||||||
|
NCM_CONFLICT = "NCM_CONFLICT"
|
||||||
|
DESCRIPTION_CONFLICT = "DESCRIPTION_CONFLICT"
|
||||||
|
PACKAGING_VARIATION = "PACKAGING_VARIATION"
|
||||||
|
FALSE_MATCH = "FALSE_MATCH"
|
||||||
|
TENANT_CONTEXT_ONLY = "TENANT_CONTEXT_ONLY"
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_product_id(observation: dict) -> str:
|
||||||
|
"""Stable global ID; fiscal and tenant fields are intentionally excluded."""
|
||||||
|
gtin = validate_gtin(observation.get("gtin"))
|
||||||
|
if gtin:
|
||||||
|
return "GTIN:" + gtin
|
||||||
|
values = "|".join((
|
||||||
|
normalize_description(observation.get("normalized_description") or observation.get("description")),
|
||||||
|
normalize_description(observation.get("brand")),
|
||||||
|
normalize_description(observation.get("unit")),
|
||||||
|
normalize_description(observation.get("package_quantity")),
|
||||||
|
))
|
||||||
|
return "FP:" + hashlib.sha256(values.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _description_equivalent(values):
|
||||||
|
stems = [token_stems(value) for value in values if value]
|
||||||
|
if not stems:
|
||||||
|
return True
|
||||||
|
return all(current == stems[0] or current.issubset(stems[0]) or stems[0].issubset(current) for current in stems[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def classify_conflict(observations: list[dict]) -> str:
|
||||||
|
"""Classify only from structural evidence; ambiguous cases remain conflicts."""
|
||||||
|
gtins = {validate_gtin(row.get("gtin")) for row in observations if validate_gtin(row.get("gtin"))}
|
||||||
|
ncms = {str(row.get("ncm")) for row in observations if row.get("ncm")}
|
||||||
|
descriptions = [row.get("normalized_description") or row.get("description") for row in observations]
|
||||||
|
packages = {str(row.get("package_quantity")) for row in observations if row.get("package_quantity")}
|
||||||
|
tenants = {str(obs.get("database") or obs.get("company_key")) for row in observations for obs in row.get("observations", [])}
|
||||||
|
if len(gtins) > 1:
|
||||||
|
return GTIN_CONFLICT
|
||||||
|
if len(ncms) > 1:
|
||||||
|
return NCM_CONFLICT
|
||||||
|
if len(packages) > 1 and _description_equivalent(descriptions):
|
||||||
|
return PACKAGING_VARIATION
|
||||||
|
if len({normalize_description(value) for value in descriptions if value}) > 1:
|
||||||
|
return DESCRIPTION_CONFLICT
|
||||||
|
if len(tenants) > 1:
|
||||||
|
return TENANT_CONTEXT_ONLY
|
||||||
|
return FALSE_MATCH
|
||||||
|
|
||||||
|
|
||||||
|
def audit_payload(payload: dict, historical_conflicts: int = 163) -> dict:
|
||||||
|
"""Audit observable same-GTIN groups without silently resolving NCM conflicts."""
|
||||||
|
by_gtin = defaultdict(list)
|
||||||
|
for product in payload.get("products", []):
|
||||||
|
gtin = validate_gtin(product.get("gtin"))
|
||||||
|
if gtin:
|
||||||
|
by_gtin[gtin].append(product)
|
||||||
|
conflicts = []
|
||||||
|
counts = defaultdict(int)
|
||||||
|
for gtin, rows in by_gtin.items():
|
||||||
|
if len({row.get("ncm") for row in rows if row.get("ncm")}) <= 1:
|
||||||
|
continue
|
||||||
|
kind = classify_conflict(rows)
|
||||||
|
counts[kind] += 1
|
||||||
|
conflicts.append({
|
||||||
|
"identity": gtin,
|
||||||
|
"classification": kind,
|
||||||
|
"observations": len(rows),
|
||||||
|
"ncms": sorted({row.get("ncm") for row in rows if row.get("ncm")}),
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"artifact": "KB_CONFLICT_AUDIT_V1",
|
||||||
|
"historical_conflicts_reported": historical_conflicts,
|
||||||
|
"observable_conflicts_audited": len(conflicts),
|
||||||
|
"unobservable_historical_conflicts": max(0, historical_conflicts - len(conflicts)),
|
||||||
|
"classification_counts": dict(sorted(counts.items())),
|
||||||
|
"conflicts": conflicts,
|
||||||
|
"policy": "only structurally equivalent identities may share a canonical product; tenant fiscal context is never global knowledge",
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Deterministic prompt compaction that preserves candidate evidence."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_tokens(payload) -> int:
|
||||||
|
return max(1, len(json.dumps(payload, ensure_ascii=False, sort_keys=True)) // 4)
|
||||||
|
|
||||||
|
|
||||||
|
def compact_payload(payload):
|
||||||
|
"""Remove repeated metadata only; candidate codes/descriptions are untouched."""
|
||||||
|
compact = dict(payload)
|
||||||
|
compact.pop("debug", None)
|
||||||
|
compact.pop("duplicate_examples", None)
|
||||||
|
candidates = compact.get("official_candidates")
|
||||||
|
if isinstance(candidates, list):
|
||||||
|
seen = set()
|
||||||
|
compact["official_candidates"] = [
|
||||||
|
row for row in candidates
|
||||||
|
if not (row.get("ncm") in seen or seen.add(row.get("ncm")))
|
||||||
|
]
|
||||||
|
return compact
|
||||||
|
|
||||||
|
|
||||||
|
def measure_payloads(payloads):
|
||||||
|
before = sum(estimate_tokens(payload) for payload in payloads)
|
||||||
|
after = sum(estimate_tokens(compact_payload(payload)) for payload in payloads)
|
||||||
|
return {"items": len(payloads), "tokens_before": before, "tokens_after": after, "reduction_percent": round((1 - after / before) * 100, 2) if before else 0.0}
|
||||||
@@ -18,6 +18,14 @@ class SemanticOutputError(ValueError):
|
|||||||
"""Raised when a provider answer violates the strict structured-output schema."""
|
"""Raised when a provider answer violates the strict structured-output schema."""
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderTransportError(RuntimeError):
|
||||||
|
"""A retryable failure while contacting a semantic provider."""
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderContractError(ValueError):
|
||||||
|
"""A non-retryable provider response or protocol violation."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class StructuredResult:
|
class StructuredResult:
|
||||||
task_type: str
|
task_type: str
|
||||||
@@ -137,3 +145,11 @@ class BudgetGuard:
|
|||||||
if plan.get("estimated_cost") is None:
|
if plan.get("estimated_cost") is None:
|
||||||
return allow_unknown_cost
|
return allow_unknown_cost
|
||||||
return plan["estimated_cost"] <= self.max_run_cost_usd
|
return plan["estimated_cost"] <= self.max_run_cost_usd
|
||||||
|
|
||||||
|
def authorize_usage(self, input_tokens: int, output_tokens: int, *, prior_cost_usd: float = 0.0,
|
||||||
|
allow_unknown_cost: bool = False) -> bool:
|
||||||
|
"""Apply the same guard to actual usage before accepting a provider result."""
|
||||||
|
plan = self.estimate(input_tokens, output_tokens)
|
||||||
|
if plan.get("estimated_cost") is None:
|
||||||
|
return allow_unknown_cost
|
||||||
|
return float(prior_cost_usd) + plan["estimated_cost"] <= self.max_run_cost_usd
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Pure, review-first questionnaire helpers for local fiscal context."""
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
|
||||||
|
from central.app.context_keys import validate_entry
|
||||||
|
|
||||||
|
|
||||||
|
def _options(row):
|
||||||
|
value = row.get("VALID_OPTIONS", [])
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
value = [x.strip() for x in value.split("|") if x.strip()]
|
||||||
|
return list(value) if isinstance(value, (list, tuple, set)) else [value]
|
||||||
|
|
||||||
|
|
||||||
|
def build_questionnaire(rows):
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
scope = str(row.get("SCOPE", row.get("scope", ""))).strip().upper()
|
||||||
|
key = str(row.get("KEY", row.get("key", ""))).strip()
|
||||||
|
scope_key = str(row.get("GROUP") or row.get("NCM") or row.get("scope_key") or "").strip()
|
||||||
|
result.append({
|
||||||
|
"question_id": str(row.get("QUESTION_ID", row.get("question_id", ""))).strip(),
|
||||||
|
"database": str(row.get("DATABASE", row.get("database", ""))).strip(),
|
||||||
|
"scope": scope,
|
||||||
|
"scope_key": scope_key if scope != "COMPANY" else "",
|
||||||
|
"group": str(row.get("GROUP") or "").strip(),
|
||||||
|
"key": key,
|
||||||
|
"question": row.get("QUESTION", row.get("question", "")),
|
||||||
|
"options": _options(row),
|
||||||
|
"affected": {"products": int(row.get("PRODUCTS_AFFECTED") or row.get("products_affected") or 0), "cells": int(row.get("CELLS") or row.get("cells") or 0)},
|
||||||
|
"examples": row.get("EXAMPLES", row.get("examples", "")),
|
||||||
|
"consequence": row.get("WHAT_IT_WILL_RESOLVE", row.get("consequence", "")),
|
||||||
|
})
|
||||||
|
return sorted(result, key=lambda q: (q["database"], q["scope"], q["scope_key"], q["key"], q["question_id"]))
|
||||||
|
|
||||||
|
|
||||||
|
def group_questions(questions):
|
||||||
|
"""Group equivalent human decisions while retaining every affected question ID."""
|
||||||
|
groups = {}
|
||||||
|
for question in questions:
|
||||||
|
key = (question["database"], question["group"], question["scope_key"], question["key"], question["question"], tuple(question["options"]))
|
||||||
|
group = groups.setdefault(key, {
|
||||||
|
"group_id": "GROUP-" + str(len(groups) + 1),
|
||||||
|
"database": question["database"],
|
||||||
|
"group": question["group"],
|
||||||
|
"scope": question["scope"],
|
||||||
|
"scope_key": question["scope_key"],
|
||||||
|
"key": question["key"],
|
||||||
|
"question": question["question"],
|
||||||
|
"options": question["options"],
|
||||||
|
"question_ids": [],
|
||||||
|
"affected": {"products": 0, "cells": 0},
|
||||||
|
"examples": [],
|
||||||
|
"consequence": question["consequence"],
|
||||||
|
})
|
||||||
|
group["question_ids"].append(question["question_id"])
|
||||||
|
group["affected"]["products"] += question["affected"]["products"]
|
||||||
|
group["affected"]["cells"] += question["affected"]["cells"]
|
||||||
|
if question["examples"] and len(group["examples"]) < 5:
|
||||||
|
group["examples"].append(str(question["examples"]))
|
||||||
|
return list(groups.values())
|
||||||
|
|
||||||
|
|
||||||
|
def validate_answers(questions, answers):
|
||||||
|
entries, effects, rollback_plan = [], [], []
|
||||||
|
by_id = {q["question_id"]: q for q in questions}
|
||||||
|
for question_id, value in answers.items():
|
||||||
|
if question_id not in by_id:
|
||||||
|
raise ValueError(f"unknown question {question_id!r}; nothing was persisted")
|
||||||
|
q = by_id[question_id]
|
||||||
|
value = str(value).strip()
|
||||||
|
if q["options"] and value not in q["options"]:
|
||||||
|
raise ValueError(f"{question_id}: value must be one of {q['options']}; nothing was persisted")
|
||||||
|
entry, error = validate_entry({"scope": q["scope"], "scope_key": q["scope_key"], "key": q["key"], "value": value})
|
||||||
|
if error:
|
||||||
|
raise ValueError(f"{question_id}: {error}; nothing was persisted")
|
||||||
|
entry["company_key"] = q["database"]
|
||||||
|
entries.append(entry)
|
||||||
|
effect = {"question_id": question_id, "database": q["database"], "key": q["key"], "affected": q["affected"], "behavior": "context_entry"}
|
||||||
|
if q["key"] == "entry_scope" and value == "MIXED_OR_DYNAMIC":
|
||||||
|
effect["behavior"] = "no_static_cfop_entrada"
|
||||||
|
effects.append(effect)
|
||||||
|
if q["key"] == "store_ipi_profile" and value == "LEAVE_NOT_APPLICABLE":
|
||||||
|
rollback_plan.append({"database": q["database"], "field": "IPI", "affected_cells": q["affected"]["cells"]})
|
||||||
|
return {"entries": entries, "effects": effects, "rollback_plan": rollback_plan}
|
||||||
|
|
||||||
|
|
||||||
|
def render_json(result):
|
||||||
|
return json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def render_csv(result):
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.DictWriter(output, fieldnames=["company_key", "scope", "scope_key", "key", "value", "reason"])
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows({k: entry.get(k, "") for k in writer.fieldnames} for entry in result["entries"])
|
||||||
|
return output.getvalue()
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""Retrieval V3 evidence layer. It ranks candidates only; it never decides fiscal truth."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
|
from .fingerprint import normalize_description, token_stems
|
||||||
|
from .retrieval import commercial_tokens, hybrid_candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _char_similarity(left, right):
|
||||||
|
return SequenceMatcher(None, normalize_description(left), normalize_description(right)).ratio()
|
||||||
|
|
||||||
|
|
||||||
|
def retrieval_v3_candidates(corpus, index, description, metadata=None, k=20, lexicon=None, exclude_product_id=None):
|
||||||
|
"""Add optional product evidence to V2 while preserving V2 as the safe baseline."""
|
||||||
|
metadata = metadata or {}
|
||||||
|
base = hybrid_candidates(corpus, index, description, k=max(k, 50), lexicon=lexicon, exclude_product_id=exclude_product_id)
|
||||||
|
gtin_ncm = str(metadata.get("gtin_ncm") or "")
|
||||||
|
group_ncms = {str(value) for value in (metadata.get("group_ncms") or [])}
|
||||||
|
rarity = metadata.get("token_rarity") or {}
|
||||||
|
query_stems = commercial_tokens(description)
|
||||||
|
rows = []
|
||||||
|
for code, text, score, official_score, kb_score, tenant_count in base:
|
||||||
|
evidence = 0.0
|
||||||
|
if gtin_ncm and code == gtin_ncm:
|
||||||
|
evidence += 1.0
|
||||||
|
if code in group_ncms:
|
||||||
|
evidence += 0.05
|
||||||
|
matched_rarity = sum(float(rarity.get(token, 1.0)) for token in query_stems if token in token_stems(text))
|
||||||
|
evidence += min(0.05, matched_rarity / 100.0)
|
||||||
|
rows.append((code, text, round(score + evidence, 4), official_score, kb_score, tenant_count))
|
||||||
|
rows.sort(key=lambda row: (-row[2], row[0]))
|
||||||
|
return rows[:k]
|
||||||
|
|
||||||
|
|
||||||
|
def candidate_fingerprint(payload):
|
||||||
|
return hashlib.sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_candidate_set(work_item, candidates, *, dataset_versions=None, gtin_evidence=None, kb_neighbors=None, hierarchical_context=None):
|
||||||
|
"""Build the final LLM-ready evidence package without selecting a candidate."""
|
||||||
|
payload = {
|
||||||
|
"task_type": work_item.task_type,
|
||||||
|
"dedup_key": work_item.dedup_key,
|
||||||
|
"normalized_product": normalize_description(work_item.payload.get("description", "")),
|
||||||
|
"top20_candidate_ncms": [
|
||||||
|
{"ncm": row[0], "official_description": row[1], "score": row[2], "official_score": row[3], "kb_score": row[4], "tenant_count": row[5]}
|
||||||
|
for row in candidates[:20]
|
||||||
|
],
|
||||||
|
"official_descriptions": {row[0]: row[1] for row in candidates[:20]},
|
||||||
|
"hierarchical_context": hierarchical_context or {},
|
||||||
|
"kb_neighbors": kb_neighbors or [],
|
||||||
|
"gtin_evidence": gtin_evidence or {},
|
||||||
|
"dataset_versions": dict(dataset_versions or {}),
|
||||||
|
}
|
||||||
|
payload["input_fingerprint"] = candidate_fingerprint(payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def compact_product_role_context(rows, limit=12):
|
||||||
|
"""Keep representative role evidence bounded; no role is inferred."""
|
||||||
|
unique = []
|
||||||
|
seen = set()
|
||||||
|
for row in rows:
|
||||||
|
description = normalize_description(row.get("description"))
|
||||||
|
if not description or description in seen:
|
||||||
|
continue
|
||||||
|
seen.add(description)
|
||||||
|
unique.append({"description": description, "group": row.get("group"), "subgroup": row.get("subgroup"), "operations": row.get("operations", [])})
|
||||||
|
if len(unique) >= limit:
|
||||||
|
break
|
||||||
|
return {"examples": unique, "evidence_only": True, "decision": None}
|
||||||
@@ -21,9 +21,10 @@ class WorkItem:
|
|||||||
kb_product_id: int | None = None
|
kb_product_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def build_work_items(questions, product_fingerprints=None) -> list[WorkItem]:
|
def build_work_items(questions, product_fingerprints=None, canonical_products=None) -> list[WorkItem]:
|
||||||
"""`product_fingerprints` maps "{DATABASE}:{erp_code}" -> KB fingerprint (from the seed pass)."""
|
"""`product_fingerprints` maps "{DATABASE}:{erp_code}" -> KB fingerprint (from the seed pass)."""
|
||||||
product_fingerprints = product_fingerprints or {}
|
product_fingerprints = product_fingerprints or {}
|
||||||
|
canonical_products = canonical_products or {}
|
||||||
items: dict = {}
|
items: dict = {}
|
||||||
|
|
||||||
def _get(task_type, dedup_key, ref, cells):
|
def _get(task_type, dedup_key, ref, cells):
|
||||||
@@ -50,6 +51,7 @@ def build_work_items(questions, product_fingerprints=None) -> list[WorkItem]:
|
|||||||
database = question.get("DATABASE", "")
|
database = question.get("DATABASE", "")
|
||||||
erp_code = str(question.get("DATABASE_CODE") or "").strip()
|
erp_code = str(question.get("DATABASE_CODE") or "").strip()
|
||||||
fingerprint = product_fingerprints.get(f"{database}:{erp_code}")
|
fingerprint = product_fingerprints.get(f"{database}:{erp_code}")
|
||||||
|
fingerprint = canonical_products.get(f"{database}:{erp_code}", fingerprint)
|
||||||
dedup = f"invalid_ncm:{fingerprint}" if fingerprint else f"invalid_ncm:{database}:{erp_code}"
|
dedup = f"invalid_ncm:{fingerprint}" if fingerprint else f"invalid_ncm:{database}:{erp_code}"
|
||||||
item = _get("INVALID_NCM", dedup, ref, cells)
|
item = _get("INVALID_NCM", dedup, ref, cells)
|
||||||
item.payload.setdefault("erp_code", erp_code)
|
item.payload.setdefault("erp_code", erp_code)
|
||||||
@@ -58,7 +60,8 @@ def build_work_items(questions, product_fingerprints=None) -> list[WorkItem]:
|
|||||||
company = question.get("COMPANY", "")
|
company = question.get("COMPANY", "")
|
||||||
group = str(question.get("GROUP") or "").strip()
|
group = str(question.get("GROUP") or "").strip()
|
||||||
if group:
|
if group:
|
||||||
item = _get("PRODUCT_ROLE", f"product_role:{company}:{group}", ref, cells)
|
normalized_group = " ".join(str(group).upper().split())
|
||||||
|
item = _get("PRODUCT_ROLE", f"product_role:{company}:{normalized_group}", ref, cells)
|
||||||
item.payload.setdefault("company_key", company)
|
item.payload.setdefault("company_key", company)
|
||||||
item.payload["group"] = group
|
item.payload["group"] = group
|
||||||
return list(items.values())
|
return list(items.values())
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""Create the conservative KB conflict audit from the read-only seed payload."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from central.app.ai.kb_consolidation import audit_payload
|
||||||
|
|
||||||
|
|
||||||
|
def main(payload_path="docs/runs/ai_kb_seed_payload.json", output="docs/KB_CONFLICT_AUDIT_V1.json"):
|
||||||
|
payload = json.loads(Path(payload_path).read_text(encoding="utf-8"))
|
||||||
|
report = audit_payload(payload)
|
||||||
|
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--payload-path", default="docs/runs/ai_kb_seed_payload.json")
|
||||||
|
parser.add_argument("--output", default="docs/KB_CONFLICT_AUDIT_V1.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(json.dumps(main(args.payload_path, args.output), indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Prepare versioned candidate evidence from already computed deterministic candidates."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from central.app.ai.retrieval_v3 import prepare_candidate_set
|
||||||
|
|
||||||
|
|
||||||
|
def main(input_path, output_path):
|
||||||
|
source = json.loads(Path(input_path).read_text(encoding="utf-8"))
|
||||||
|
prepared = []
|
||||||
|
for item in source.get("items", []):
|
||||||
|
prepared.append(prepare_candidate_set(
|
||||||
|
type("WorkItem", (), {"task_type": item.get("task_type", "INVALID_NCM"), "dedup_key": item["dedup_key"], "payload": item.get("payload", {})})(),
|
||||||
|
[tuple(row) for row in item.get("candidates", [])],
|
||||||
|
dataset_versions=source.get("dataset_versions"),
|
||||||
|
gtin_evidence=item.get("gtin_evidence"),
|
||||||
|
kb_neighbors=item.get("kb_neighbors"),
|
||||||
|
hierarchical_context=item.get("hierarchical_context"),
|
||||||
|
))
|
||||||
|
report = {"artifact": "AI_CANDIDATE_PREPARATION_V3", "items": prepared, "semantic_decisions": 0}
|
||||||
|
Path(output_path).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("input_path")
|
||||||
|
parser.add_argument("output_path")
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(json.dumps(main(args.input_path, args.output_path), indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Offline reprocessing of residual questions against the final canonical identity map."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from central.app.ai.kb_consolidation import canonical_product_id
|
||||||
|
from central.app.ai.workitems import build_work_items
|
||||||
|
|
||||||
|
|
||||||
|
def _erp_code(question):
|
||||||
|
import re
|
||||||
|
match = re.search(r"PRODUCT\s+(\d+)", question.get("QUESTION", ""))
|
||||||
|
return match.group(1) if match else str(question.get("DATABASE_CODE") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def main(questions_path, payload_path, output_path):
|
||||||
|
questions_doc = json.loads(Path(questions_path).read_text(encoding="utf-8"))
|
||||||
|
payload = json.loads(Path(payload_path).read_text(encoding="utf-8"))
|
||||||
|
canonical = {}
|
||||||
|
fingerprints = {}
|
||||||
|
by_observation = defaultdict(list)
|
||||||
|
for product in payload.get("products", []):
|
||||||
|
cid = canonical_product_id(product)
|
||||||
|
for observation in product.get("observations", []):
|
||||||
|
key = f"{observation.get('database')}:{observation.get('erp_code')}"
|
||||||
|
canonical[key] = cid
|
||||||
|
fingerprints[key] = product.get("fingerprint") or cid
|
||||||
|
questions = [dict(question, DATABASE_CODE=_erp_code(question)) for question in questions_doc.get("questions", [])]
|
||||||
|
items = build_work_items(questions, fingerprints, canonical)
|
||||||
|
by_task = {}
|
||||||
|
for item in items:
|
||||||
|
bucket = by_task.setdefault(item.task_type, {"items": 0, "cells": 0})
|
||||||
|
bucket["items"] += 1
|
||||||
|
bucket["cells"] += item.cells_covered
|
||||||
|
report = {
|
||||||
|
"artifact": "AI_RESIDUAL_PLAN_V3",
|
||||||
|
"work_items_before": 750,
|
||||||
|
"work_items_after": len(items),
|
||||||
|
"llm_items_before": 511,
|
||||||
|
# Candidate preparation is evidence-only; without an official validated
|
||||||
|
# answer it must not reclassify a selection item as resolved.
|
||||||
|
"llm_items_after": 511,
|
||||||
|
"llm_reclassification": "UNCHANGED_PENDING_FINAL_PROVIDER_SELECTION",
|
||||||
|
"by_task_type": by_task,
|
||||||
|
"canonical_observation_keys": len(canonical),
|
||||||
|
"ai_write_allowed": False,
|
||||||
|
"semantic_decisions": 0,
|
||||||
|
}
|
||||||
|
Path(output_path).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--questions", default="docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json")
|
||||||
|
parser.add_argument("--payload", default="docs/runs/ai_kb_seed_payload.json")
|
||||||
|
parser.add_argument("--output", default="docs/AI_RESIDUAL_PLAN_V3.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(json.dumps(main(args.questions, args.payload, args.output), indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""Run the real evaluation plumbing without network, credentials, or Firebird writes."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from central.app.ai.batch_runner import run_batches
|
||||||
|
from central.app.ai.fake_provider import DeterministicFakeSemanticProvider
|
||||||
|
from central.app.ai.llm_provider import LlmEvalItem
|
||||||
|
|
||||||
|
|
||||||
|
def _items(prefix, count):
|
||||||
|
return [
|
||||||
|
LlmEvalItem(
|
||||||
|
work_item_id=f"{prefix}-{index:03d}",
|
||||||
|
description=f"Produto de teste {index}",
|
||||||
|
normalized_description=f"PRODUTO TESTE {index}",
|
||||||
|
brand="TESTE",
|
||||||
|
unit="UN",
|
||||||
|
segment="offline",
|
||||||
|
candidates=[(f"{index:08d}", "CANDIDATO OFICIAL PRINCIPAL", 1.0, 1.0, 0.0, 0),
|
||||||
|
(f"{index + 1:08d}", "CANDIDATO OFICIAL ALTERNATIVO", 0.5, 0.5, 0.0, 0)],
|
||||||
|
)
|
||||||
|
for index in range(count)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_phase(name, items, batch_sizes):
|
||||||
|
phase = {"items": len(items), "batch_sizes": {}, "provider_calls": 0, "retries": 0, "answered": 0, "errors": 0}
|
||||||
|
for batch_size in batch_sizes:
|
||||||
|
provider = DeterministicFakeSemanticProvider()
|
||||||
|
result = run_batches(items, provider, batch_size=batch_size)
|
||||||
|
phase["batch_sizes"][str(batch_size)] = {
|
||||||
|
"provider_calls": result.provider_calls,
|
||||||
|
"retries": result.retries,
|
||||||
|
"answered": len(result.results_by_work_item_id),
|
||||||
|
"errors": len(result.errors),
|
||||||
|
}
|
||||||
|
phase["provider_calls"] += result.provider_calls
|
||||||
|
phase["retries"] += result.retries
|
||||||
|
phase["answered"] = max(phase["answered"], len(result.results_by_work_item_id))
|
||||||
|
phase["errors"] = max(phase["errors"], len(result.errors))
|
||||||
|
phase["phase"] = name
|
||||||
|
return phase
|
||||||
|
|
||||||
|
|
||||||
|
def run_offline_ai_simulation(output_dir="docs/runs/offline_ai", scenario="CORRECT", batch_sizes=(5, 10, 20), final_work_items_path=None, final_candidates_path=None):
|
||||||
|
"""Exercise GOLD -> gate -> SILVER/NOISY -> residual plumbing only."""
|
||||||
|
if scenario != "CORRECT":
|
||||||
|
raise ValueError("E2E simulation uses CORRECT only; failure scenarios belong to focused tests")
|
||||||
|
gold = _items("gold", 65)
|
||||||
|
silver = _items("silver", 118)
|
||||||
|
noisy = _items("noisy", 17)
|
||||||
|
gold_phase = _run_phase("GOLD", gold, batch_sizes)
|
||||||
|
correct = gold_phase["answered"]
|
||||||
|
selection_accuracy = correct / len(gold) if gold else 0.0
|
||||||
|
gate = selection_accuracy >= 0.95 and gold_phase["errors"] == 0
|
||||||
|
phases = [gold_phase]
|
||||||
|
if gate:
|
||||||
|
phases.extend((_run_phase("SILVER", silver, batch_sizes), _run_phase("NOISY", noisy, batch_sizes)))
|
||||||
|
final_inputs = {}
|
||||||
|
for label, path in (("work_items", final_work_items_path), ("candidates", final_candidates_path)):
|
||||||
|
if path:
|
||||||
|
source = Path(path)
|
||||||
|
if source.exists():
|
||||||
|
final_inputs[label] = {"path": str(source), "bytes": source.stat().st_size, "loaded": True}
|
||||||
|
report = {
|
||||||
|
"status": "PASS_PLUMBING" if gate else "FAIL_PLUMBING",
|
||||||
|
"scenario": scenario,
|
||||||
|
"gold": {"count": len(gold), "selection_accuracy": selection_accuracy, "gate_opened": gate},
|
||||||
|
"phases": phases,
|
||||||
|
"residual_plan": {"executed": gate, "semantic_results_accepted": False, "llm_selection_required": 511},
|
||||||
|
"external_calls": 0,
|
||||||
|
"firebird_ai_writes": 0,
|
||||||
|
"ai_write_allowed": False,
|
||||||
|
"fake_results_are_authoritative": False,
|
||||||
|
"final_inputs": final_inputs,
|
||||||
|
}
|
||||||
|
path = Path(output_dir) / "OFFLINE_AI_SIMULATION_V1.json"
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--output-dir", default="docs/runs/offline_ai")
|
||||||
|
parser.add_argument("--final-work-items")
|
||||||
|
parser.add_argument("--final-candidates")
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(json.dumps(run_offline_ai_simulation(args.output_dir, final_work_items_path=args.final_work_items, final_candidates_path=args.final_candidates), indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Compare V3 retrieval policy with the recorded V2 holdout without retuning on GOLD rows."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main(v2_path="docs/NCM_RETRIEVAL_EVALUATION_V2.json", output="docs/NCM_RETRIEVAL_EVALUATION_V3.json"):
|
||||||
|
baseline = json.loads(Path(v2_path).read_text(encoding="utf-8"))
|
||||||
|
gold = baseline.get("by_quality", {}).get("GOLD", {}).get("HYBRID", {})
|
||||||
|
# V3 is deliberately not activated from a synthetic or incomplete replay.
|
||||||
|
# Its optional evidence layer is covered by unit tests and retains V2 ordering
|
||||||
|
# when no new evidence is supplied.
|
||||||
|
report = {
|
||||||
|
"artifact": "NCM_RETRIEVAL_EVALUATION_V3",
|
||||||
|
"baseline_artifact": "NCM_RETRIEVAL_EVALUATION_V2",
|
||||||
|
"activation": "NOT_ACTIVATED_PENDING_FULL_HOLDOUT_REPLAY",
|
||||||
|
"gold_recall_at_1_before": gold.get("recall_at_1"),
|
||||||
|
"gold_recall_at_20_before": gold.get("recall_at_20"),
|
||||||
|
"gold_mrr_before": gold.get("mrr"),
|
||||||
|
"gold_recall_at_1_after": gold.get("recall_at_1"),
|
||||||
|
"gold_recall_at_20_after": gold.get("recall_at_20"),
|
||||||
|
"gold_mrr_after": gold.get("mrr"),
|
||||||
|
"local_embeddings_used": False,
|
||||||
|
"local_embeddings_improvement": None,
|
||||||
|
"evidence_features": ["BM25-compatible lexical baseline", "word/character evidence hooks", "token rarity", "hierarchy", "KB neighbors", "GTIN", "product group"],
|
||||||
|
"policy": "retain V2 until a complete untouched holdout replay proves recall_at_20 >= 0.954 and no regression",
|
||||||
|
}
|
||||||
|
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--v2", default="docs/NCM_RETRIEVAL_EVALUATION_V2.json")
|
||||||
|
parser.add_argument("--output", default="docs/NCM_RETRIEVAL_EVALUATION_V3.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(json.dumps(main(args.v2, args.output), indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from central.app.ai.cache_policy import CONFLICT, NEEDS_REVALIDATION, VALID, cache_key, classify_knowledge_freshness
|
||||||
|
from central.app.ai.provider import BudgetGuard
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_key_changes_for_each_input_dimension():
|
||||||
|
base = ("input", "candidates", "model", "prompt", {"ncm": "n1", "tipi": "t1"})
|
||||||
|
key = cache_key(*base)
|
||||||
|
assert key != cache_key("changed", *base[1:])
|
||||||
|
assert key != cache_key(base[0], "changed", *base[2:])
|
||||||
|
assert key != cache_key(base[0], base[1], "changed", *base[3:])
|
||||||
|
assert key != cache_key(base[0], base[1], base[2], "changed", base[4])
|
||||||
|
assert key != cache_key(*base[:4], {"ncm": "n2", "tipi": "t1"})
|
||||||
|
assert key == cache_key(*base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataset_freshness_is_fail_closed():
|
||||||
|
assert classify_knowledge_freshness({"ncm": "v1"}, {"ncm": "v1"}) == VALID
|
||||||
|
assert classify_knowledge_freshness({"ncm": "v1"}, {"ncm": "v2"}) == CONFLICT
|
||||||
|
assert classify_knowledge_freshness({"ncm": "v1"}, {"ncm": "v1", "tipi": "v2"}) == NEEDS_REVALIDATION
|
||||||
|
assert classify_knowledge_freshness({}, {"ncm": "v1"}) == NEEDS_REVALIDATION
|
||||||
|
|
||||||
|
|
||||||
|
def test_budget_blocks_before_call_and_actual_usage_cannot_exceed_limit():
|
||||||
|
guard = BudgetGuard(1.0, 1.0, 2.0)
|
||||||
|
assert guard.authorize(guard.estimate(600_000, 300_000)) is False
|
||||||
|
assert guard.authorize(guard.estimate(100_000, 50_000)) is True
|
||||||
|
assert guard.authorize_usage(100_000, 50_000, prior_cost_usd=0.9) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_cost_requires_explicit_limited_run():
|
||||||
|
guard = BudgetGuard(5.0, None, None)
|
||||||
|
assert guard.authorize_usage(1000, 100) is False
|
||||||
|
assert guard.authorize_usage(1000, 100, allow_unknown_cost=True) is True
|
||||||
@@ -99,6 +99,16 @@ def test_work_items_dedup_by_knowledge_needed():
|
|||||||
assert all(i.task_type in ("INVALID_NCM", "CEST", "EX_TIPI", "PRODUCT_ROLE") for i in items)
|
assert all(i.task_type in ("INVALID_NCM", "CEST", "EX_TIPI", "PRODUCT_ROLE") for i in items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_work_items_accept_canonical_identity_map():
|
||||||
|
from central.app.ai.workitems import build_work_items
|
||||||
|
questions = [
|
||||||
|
{"QUESTION_ID": "Q1", "DATABASE": "A", "SCOPE": "PRODUCT", "KEY": "ncm", "CELLS": 1, "DATABASE_CODE": "1"},
|
||||||
|
{"QUESTION_ID": "Q2", "DATABASE": "B", "SCOPE": "PRODUCT", "KEY": "ncm", "CELLS": 1, "DATABASE_CODE": "2"},
|
||||||
|
]
|
||||||
|
items = build_work_items(questions, canonical_products={"A:1": "GTIN:7891234567895", "B:2": "GTIN:7891234567895"})
|
||||||
|
assert len(items) == 1 and items[0].cells_covered == 2
|
||||||
|
|
||||||
|
|
||||||
# ---------------- provider contract / structured output / budget ----------------
|
# ---------------- provider contract / structured output / budget ----------------
|
||||||
|
|
||||||
def test_structured_output_schema_and_baseline_provider():
|
def test_structured_output_schema_and_baseline_provider():
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from central.app.ai.batch_runner import run_batches
|
||||||
|
from central.app.ai.fake_provider import DeterministicFakeSemanticProvider
|
||||||
|
from central.app.ai.llm_provider import LlmEvalItem
|
||||||
|
|
||||||
|
|
||||||
|
def _item(work_item_id):
|
||||||
|
return LlmEvalItem(
|
||||||
|
work_item_id=work_item_id,
|
||||||
|
description=work_item_id,
|
||||||
|
normalized_description=work_item_id,
|
||||||
|
brand=None,
|
||||||
|
unit=None,
|
||||||
|
segment="test",
|
||||||
|
candidates=[("A", "candidate A", 1.0), ("B", "candidate B", 0.5)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("batch_size", [5, 10, 20])
|
||||||
|
def test_run_batches_correlates_shuffled_answers_by_id(batch_size):
|
||||||
|
items = [_item(f"w{i}") for i in range(batch_size + 1)]
|
||||||
|
provider = DeterministicFakeSemanticProvider(shuffle=True)
|
||||||
|
|
||||||
|
result = run_batches(items, provider, batch_size=batch_size)
|
||||||
|
|
||||||
|
assert set(result.results_by_work_item_id) == {item.work_item_id for item in items}
|
||||||
|
assert all(result.results_by_work_item_id[item.work_item_id]["work_item_id"] == item.work_item_id for item in items)
|
||||||
|
assert result.errors == {}
|
||||||
|
assert result.provider_calls == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("scenario", ["MISSING_ITEM", "DUPLICATE_ITEM", "INVALID_JSON", "OUT_OF_CANDIDATE"])
|
||||||
|
def test_run_batches_preserves_failed_ids_for_contract_errors(scenario):
|
||||||
|
items = [_item("w1"), _item("w2")]
|
||||||
|
provider = DeterministicFakeSemanticProvider()
|
||||||
|
|
||||||
|
result = run_batches(items, provider, batch_size=2, scenario=scenario)
|
||||||
|
|
||||||
|
assert result.results_by_work_item_id == {}
|
||||||
|
assert set(result.errors) == {"w1", "w2"}
|
||||||
|
assert result.retries == 0
|
||||||
|
assert result.provider_calls == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("scenario", ["TIMEOUT", "429", "500"])
|
||||||
|
def test_run_batches_retries_transport_errors_and_preserves_failed_ids(scenario):
|
||||||
|
items = [_item("w1"), _item("w2")]
|
||||||
|
provider = DeterministicFakeSemanticProvider()
|
||||||
|
|
||||||
|
result = run_batches(items, provider, batch_size=2, max_retries=2, scenario=scenario)
|
||||||
|
|
||||||
|
assert result.results_by_work_item_id == {}
|
||||||
|
assert set(result.errors) == {"w1", "w2"}
|
||||||
|
assert result.provider_calls == 3
|
||||||
|
assert result.retries == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_batches_allows_partial_failure_by_item():
|
||||||
|
items = [_item("w1"), _item("w2"), _item("w3")]
|
||||||
|
provider = DeterministicFakeSemanticProvider(fail_work_item_ids={"w2"}, failure_scenario="TIMEOUT")
|
||||||
|
|
||||||
|
result = run_batches(items, provider, batch_size=1, max_retries=1)
|
||||||
|
|
||||||
|
assert set(result.results_by_work_item_id) == {"w1", "w3"}
|
||||||
|
assert set(result.errors) == {"w2"}
|
||||||
|
assert result.provider_calls == 4
|
||||||
|
assert result.retries == 1
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import copy
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from central.app.ai.fake_provider import DeterministicFakeSemanticProvider
|
||||||
|
from central.app.ai.llm_provider import LlmAnswerError, LlmEvalItem
|
||||||
|
from central.app.ai.provider import ProviderContractError, ProviderTransportError
|
||||||
|
|
||||||
|
|
||||||
|
def _item(work_item_id="w1"):
|
||||||
|
return LlmEvalItem(
|
||||||
|
work_item_id=work_item_id,
|
||||||
|
description="AGUA MINERAL 500ML",
|
||||||
|
normalized_description="AGUA MINERAL 500ML",
|
||||||
|
brand=None,
|
||||||
|
unit="ML",
|
||||||
|
segment="mercado",
|
||||||
|
candidates=[("22011000", "AGUAS MINERAIS", 0.9), ("22019000", "OUTRAS AGUAS", 0.4)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("scenario", "selected", "abstain"),
|
||||||
|
[
|
||||||
|
("CORRECT", "22011000", False),
|
||||||
|
("WRONG", "22019000", False),
|
||||||
|
("ABSTAIN", None, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_fake_provider_returns_deterministic_valid_answers(scenario, selected, abstain):
|
||||||
|
provider = DeterministicFakeSemanticProvider()
|
||||||
|
|
||||||
|
answers = provider.select_batch([_item()], scenario=scenario)
|
||||||
|
|
||||||
|
assert answers == provider.select_batch([_item()], scenario=scenario)
|
||||||
|
assert answers[0]["work_item_id"] == "w1"
|
||||||
|
assert answers[0]["selected_ncm"] == selected
|
||||||
|
assert answers[0]["abstain"] is abstain
|
||||||
|
assert provider.calls == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("scenario", ["INVALID_JSON", "MISSING_ITEM", "DUPLICATE_ITEM", "OUT_OF_CANDIDATE"])
|
||||||
|
def test_fake_provider_rejects_contract_scenarios_without_mutating_items(scenario):
|
||||||
|
provider = DeterministicFakeSemanticProvider()
|
||||||
|
items = [_item("w1"), _item("w2")]
|
||||||
|
original = copy.deepcopy(items)
|
||||||
|
|
||||||
|
with pytest.raises((LlmAnswerError, ProviderContractError)):
|
||||||
|
provider.select_batch(items, scenario=scenario)
|
||||||
|
|
||||||
|
assert items == original
|
||||||
|
assert provider.calls == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("scenario", ["TIMEOUT", "429", "500"])
|
||||||
|
def test_fake_provider_raises_typed_transport_errors(scenario):
|
||||||
|
provider = DeterministicFakeSemanticProvider()
|
||||||
|
|
||||||
|
with pytest.raises(ProviderTransportError):
|
||||||
|
provider.select_batch([_item()], scenario=scenario)
|
||||||
|
|
||||||
|
assert provider.calls == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fake_provider_rejects_unknown_scenarios():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DeterministicFakeSemanticProvider().select_batch([_item()], scenario="UNKNOWN")
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
def test_canonical_identity_excludes_tenant_fiscal_context():
|
||||||
|
from central.app.ai.kb_consolidation import canonical_product_id
|
||||||
|
|
||||||
|
a = canonical_product_id({"gtin": "7891234567895", "ncm": "22011000", "database": "A"})
|
||||||
|
b = canonical_product_id({"gtin": "7891234567895", "ncm": "22019000", "database": "B"})
|
||||||
|
assert a == b == "GTIN:7891234567895"
|
||||||
|
|
||||||
|
|
||||||
|
def test_conflicts_are_conservative():
|
||||||
|
from central.app.ai.kb_consolidation import NCM_CONFLICT, PACKAGING_VARIATION, classify_conflict
|
||||||
|
|
||||||
|
assert classify_conflict([
|
||||||
|
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA MINERAL", "package_quantity": "500"},
|
||||||
|
{"gtin": "7891234567895", "ncm": "22019000", "normalized_description": "AGUA MINERAL", "package_quantity": "500"},
|
||||||
|
]) == NCM_CONFLICT
|
||||||
|
assert classify_conflict([
|
||||||
|
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA MINERAL", "package_quantity": "500"},
|
||||||
|
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA MINERAL", "package_quantity": "1L"},
|
||||||
|
]) == PACKAGING_VARIATION
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_audit_reports_observable_coverage():
|
||||||
|
from central.app.ai.kb_consolidation import audit_payload
|
||||||
|
|
||||||
|
report = audit_payload({"products": [
|
||||||
|
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA", "observations": [{"database": "A"}]},
|
||||||
|
{"gtin": "7891234567895", "ncm": "22019000", "normalized_description": "AGUA", "observations": [{"database": "B"}]},
|
||||||
|
]}, historical_conflicts=163)
|
||||||
|
assert report["historical_conflicts_reported"] == 163
|
||||||
|
assert report["observable_conflicts_audited"] == 1
|
||||||
|
assert report["unobservable_historical_conflicts"] == 162
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def test_offline_simulation_runs_full_plumbing_without_accepting_fake_answers(tmp_path):
|
||||||
|
from central.scripts.run_offline_ai_simulation import run_offline_ai_simulation
|
||||||
|
|
||||||
|
report = run_offline_ai_simulation(tmp_path)
|
||||||
|
assert report["status"] == "PASS_PLUMBING"
|
||||||
|
assert report["gold"]["gate_opened"] is True
|
||||||
|
assert [phase["phase"] for phase in report["phases"]] == ["GOLD", "SILVER", "NOISY"]
|
||||||
|
assert report["external_calls"] == 0
|
||||||
|
assert report["firebird_ai_writes"] == 0
|
||||||
|
assert report["residual_plan"]["semantic_results_accepted"] is False
|
||||||
|
saved = json.loads((tmp_path / "OFFLINE_AI_SIMULATION_V1.json").read_text(encoding="utf-8"))
|
||||||
|
assert saved == report
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
def test_payload_compaction_preserves_candidate_codes_and_measures_reduction():
|
||||||
|
from central.app.ai.payloads import compact_payload, measure_payloads
|
||||||
|
|
||||||
|
payload = {"description": "agua", "debug": "remove", "official_candidates": [{"ncm": "22011000", "text": "x"}, {"ncm": "22011000", "text": "x"}, {"ncm": "22019000", "text": "y"}]}
|
||||||
|
compact = compact_payload(payload)
|
||||||
|
assert [row["ncm"] for row in compact["official_candidates"]] == ["22011000", "22019000"]
|
||||||
|
assert "debug" not in compact
|
||||||
|
assert measure_payloads([payload])["tokens_after"] < measure_payloads([payload])["tokens_before"]
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from central.app.ai.questionnaire import build_questionnaire, group_questions, validate_answers
|
||||||
|
|
||||||
|
|
||||||
|
def test_questionnaire_groups_by_scope_and_preserves_effects():
|
||||||
|
rows = [
|
||||||
|
{"QUESTION_ID": "Q1", "DATABASE": "ACME", "SCOPE": "COMPANY", "GROUP": "", "KEY": "entry_scope",
|
||||||
|
"VALID_OPTIONS": ["INTRA", "INTER", "MIXED_OR_DYNAMIC"], "PRODUCTS_AFFECTED": 2, "CELLS": 3,
|
||||||
|
"QUESTION": "Escopo?", "EXAMPLES": "P1; P2"},
|
||||||
|
{"QUESTION_ID": "Q2", "DATABASE": "ACME", "SCOPE": "GROUP", "GROUP": "G1", "KEY": "product_role",
|
||||||
|
"VALID_OPTIONS": ["RESALE"], "PRODUCTS_AFFECTED": 1, "CELLS": 1, "QUESTION": "Papel?"},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = build_questionnaire(rows)
|
||||||
|
|
||||||
|
assert [item["question_id"] for item in result] == ["Q1", "Q2"]
|
||||||
|
assert result[0]["affected"] == {"products": 2, "cells": 3}
|
||||||
|
assert "MIXED_OR_DYNAMIC" in result[0]["options"]
|
||||||
|
assert result[1]["scope_key"] == "G1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_answers_are_validated_without_persisting_and_mixed_scope_is_explicit():
|
||||||
|
questions = build_questionnaire([{
|
||||||
|
"QUESTION_ID": "Q1", "DATABASE": "ACME", "SCOPE": "COMPANY", "GROUP": "", "KEY": "entry_scope",
|
||||||
|
"VALID_OPTIONS": ["INTRA", "INTER", "MIXED_OR_DYNAMIC"], "QUESTION": "Escopo?",
|
||||||
|
}])
|
||||||
|
|
||||||
|
answers = validate_answers(questions, {"Q1": "MIXED_OR_DYNAMIC"})
|
||||||
|
|
||||||
|
assert answers["entries"] == [{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC", "reason": None, "company_key": "ACME"}]
|
||||||
|
assert answers["effects"][0]["behavior"] == "no_static_cfop_entrada"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="nothing was persisted"):
|
||||||
|
validate_answers(questions, {"Q1": "OUTSIDE"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_ipi_profile_requires_explicit_choice_and_only_plans_rollback():
|
||||||
|
questions = build_questionnaire([{
|
||||||
|
"QUESTION_ID": "QIPI-ACME", "DATABASE": "ACME", "SCOPE": "COMPANY", "GROUP": "", "KEY": "store_ipi_profile",
|
||||||
|
"VALID_OPTIONS": ["POPULATE_REFERENCE_RATE", "LEAVE_NOT_APPLICABLE"], "QUESTION": "IPI?",
|
||||||
|
"PRODUCTS_AFFECTED": 4, "CELLS": 4,
|
||||||
|
}])
|
||||||
|
|
||||||
|
result = validate_answers(questions, {"QIPI-ACME": "LEAVE_NOT_APPLICABLE"})
|
||||||
|
|
||||||
|
assert result["entries"][0]["value"] == "LEAVE_NOT_APPLICABLE"
|
||||||
|
assert result["rollback_plan"] == [{"database": "ACME", "field": "IPI", "affected_cells": 4}]
|
||||||
|
assert json.dumps(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_equivalent_questions_are_grouped_with_aggregated_impact():
|
||||||
|
questions = build_questionnaire([
|
||||||
|
{"QUESTION_ID": "Q1", "DATABASE": "ACME", "SCOPE": "GROUP", "GROUP": "REFRIGERANTES", "KEY": "product_role", "VALID_OPTIONS": ["RESALE"], "CELLS": 10, "PRODUCTS_AFFECTED": 3, "QUESTION": "Qual o papel?"},
|
||||||
|
{"QUESTION_ID": "Q2", "DATABASE": "ACME", "SCOPE": "GROUP", "GROUP": "REFRIGERANTES", "KEY": "product_role", "VALID_OPTIONS": ["RESALE"], "CELLS": 20, "PRODUCTS_AFFECTED": 5, "QUESTION": "Qual o papel?"},
|
||||||
|
])
|
||||||
|
groups = group_questions(questions)
|
||||||
|
assert len(groups) == 1 and groups[0]["question_ids"] == ["Q1", "Q2"]
|
||||||
|
assert groups[0]["affected"] == {"products": 8, "cells": 30}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
def test_retrieval_v3_gtin_evidence_is_candidate_only():
|
||||||
|
from central.app.ai.retrieval_v3 import retrieval_v3_candidates
|
||||||
|
|
||||||
|
class Corpus:
|
||||||
|
pass
|
||||||
|
corpus = Corpus()
|
||||||
|
corpus.documents = {}
|
||||||
|
corpus.descriptions = {}
|
||||||
|
corpus.positions = {}
|
||||||
|
corpus.children = {}
|
||||||
|
class Index:
|
||||||
|
entries = []
|
||||||
|
# Empty V2 corpus cannot manufacture an NCM, proving no candidate is invented.
|
||||||
|
assert retrieval_v3_candidates(corpus, Index(), "produto", {"gtin_ncm": "22011000"}) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_candidate_set_is_fingerprinted_and_non_decisional():
|
||||||
|
from central.app.ai.retrieval_v3 import prepare_candidate_set
|
||||||
|
|
||||||
|
work = type("WorkItem", (), {"task_type": "INVALID_NCM", "dedup_key": "x", "payload": {"description": "Agua mineral"}})()
|
||||||
|
prepared = prepare_candidate_set(work, [("22011000", "AGUAS MINERAIS", 0.9, 0.8, 0.1, 2)], dataset_versions={"ncm": "v1"})
|
||||||
|
assert prepared["top20_candidate_ncms"][0]["ncm"] == "22011000"
|
||||||
|
assert prepared["input_fingerprint"]
|
||||||
|
assert "selected_ncm" not in prepared
|
||||||
|
|
||||||
|
|
||||||
|
def test_product_role_context_is_compact_and_evidence_only():
|
||||||
|
from central.app.ai.retrieval_v3 import compact_product_role_context
|
||||||
|
|
||||||
|
result = compact_product_role_context([{"description": "Coca Cola"}, {"description": "coca-cola"}, {"description": "Pepsi"}], limit=2)
|
||||||
|
assert len(result["examples"]) == 2 and result["decision"] is None and result["evidence_only"] is True
|
||||||
@@ -422,3 +422,25 @@ Pilot profile: CONSTRUCAO (`pilot-construcao.env.example`); planned, not install
|
|||||||
|
|
||||||
## Local Completion V2 (2026-09-20)
|
## Local Completion V2 (2026-09-20)
|
||||||
Audit outcome in `docs/LOCAL_COMPLETION_V2_REPORT.md`: origin rule invalid → rolled back (35,048); CFOP/IPPT weak-evidence writes rolled back; role no longer inferred from PROD_COZINHA alone; SPED 4.3.x dataset in `docs/sources/`; PIS/COFINS Simples rule from ERP emission evidence; questionnaire V2 (`docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.*`). Rollback tool: `scripts/rollback_field.py`; audit: `scripts/audit_local_writes.py`.
|
Audit outcome in `docs/LOCAL_COMPLETION_V2_REPORT.md`: origin rule invalid → rolled back (35,048); CFOP/IPPT weak-evidence writes rolled back; role no longer inferred from PROD_COZINHA alone; SPED 4.3.x dataset in `docs/sources/`; PIS/COFINS Simples rule from ERP emission evidence; questionnaire V2 (`docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.*`). Rollback tool: `scripts/rollback_field.py`; audit: `scripts/audit_local_writes.py`.
|
||||||
|
## Local Completion V3 Operations
|
||||||
|
|
||||||
|
Tasks 8-10 are CLI-first and review-first: company/group answers are validated
|
||||||
|
locally and emitted as JSON/CSV without import or Firebird writes; `MIXED_OR_DYNAMIC`
|
||||||
|
never invents a static entry CFOP; `STORE_IPI_PROFILE` requires an explicit choice
|
||||||
|
and produces only a rollback plan. Rollback previews are scoped by run, database,
|
||||||
|
field, and product and refuse drift where current differs from the audited after
|
||||||
|
value. `scripts/run_local_validation.py` and `scripts/generate_local_health.py`
|
||||||
|
are deterministic and emit `docs/LOCAL_PRODUCT_HEALTH_V1.json` with measured counts.
|
||||||
|
|
||||||
|
## PRE-AI Local Completion V3 (2026-09-21)
|
||||||
|
|
||||||
|
Offline completion is operationally ready. `DeterministicFakeSemanticProvider`
|
||||||
|
and the ID-correlated batch runner cover batch sizes 5/10/20 plus contract and
|
||||||
|
transport failures; budget, cache-key, dataset-drift, and zero-write E2E tests
|
||||||
|
are present. KB conflict audit is conservative: 90 observable GTIN/NCM conflicts
|
||||||
|
remain `NCM_CONFLICT`; 73 historical conflicts are explicitly uncovered by the
|
||||||
|
seed payload. Canonical identity is deterministic and excludes tenant fiscal
|
||||||
|
context. Retrieval V3 evidence hooks exist but V2 remains active until a full
|
||||||
|
untouched holdout replay proves no regression. Human/group questionnaire CLIs,
|
||||||
|
guarded rollback preview, local validation, and health report are available.
|
||||||
|
See `docs/PRE_AI_LOCAL_COMPLETION_V3_REPORT.md` for measured values.
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"artifact": "AI_RESIDUAL_PLAN_V3",
|
||||||
|
"work_items_before": 750,
|
||||||
|
"work_items_after": 750,
|
||||||
|
"llm_items_before": 511,
|
||||||
|
"llm_items_after": 511,
|
||||||
|
"llm_reclassification": "UNCHANGED_PENDING_FINAL_PROVIDER_SELECTION",
|
||||||
|
"by_task_type": {
|
||||||
|
"EX_TIPI": {
|
||||||
|
"items": 112,
|
||||||
|
"cells": 4130
|
||||||
|
},
|
||||||
|
"CEST": {
|
||||||
|
"items": 353,
|
||||||
|
"cells": 2045
|
||||||
|
},
|
||||||
|
"PRODUCT_ROLE": {
|
||||||
|
"items": 41,
|
||||||
|
"cells": 482
|
||||||
|
},
|
||||||
|
"INVALID_NCM": {
|
||||||
|
"items": 244,
|
||||||
|
"cells": 1970
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"canonical_observation_keys": 35065,
|
||||||
|
"ai_write_allowed": false,
|
||||||
|
"semantic_decisions": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,912 @@
|
|||||||
|
{
|
||||||
|
"artifact": "KB_CONFLICT_AUDIT_V1",
|
||||||
|
"historical_conflicts_reported": 163,
|
||||||
|
"observable_conflicts_audited": 90,
|
||||||
|
"unobservable_historical_conflicts": 73,
|
||||||
|
"classification_counts": {
|
||||||
|
"NCM_CONFLICT": 90
|
||||||
|
},
|
||||||
|
"conflicts": [
|
||||||
|
{
|
||||||
|
"identity": "7908429102622",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"40169990",
|
||||||
|
"87141000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898324238536",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85393100",
|
||||||
|
"85393111"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898324002410",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85393100",
|
||||||
|
"85393111"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "4047025341110",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"27101932",
|
||||||
|
"38190000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "4047025341127",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"27101932",
|
||||||
|
"38190000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898227950016",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"27101932",
|
||||||
|
"28070010"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891117007128",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"82015000",
|
||||||
|
"82130000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891117097921",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"82015000",
|
||||||
|
"82130000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899882310993",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"38101010",
|
||||||
|
"85094020"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891114063523",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"82041100",
|
||||||
|
"95051000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891117003144",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"72162100",
|
||||||
|
"82014000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7909224158333",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"64019200",
|
||||||
|
"73269090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898042710352",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"38089198",
|
||||||
|
"38089199"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898049710157",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22011000",
|
||||||
|
"38089192"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896673107206",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"38245000",
|
||||||
|
"68052000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896673107213",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"38245000",
|
||||||
|
"68052000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899737517201",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"82014000",
|
||||||
|
"82019000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7892679250021",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22011000",
|
||||||
|
"87141000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7892679200361",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"21039021",
|
||||||
|
"87141000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898994318712",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"07096000",
|
||||||
|
"82084000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891114093155",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"07049000",
|
||||||
|
"82041100"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898904146206",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"38085910",
|
||||||
|
"38089119"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891114010961",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"82054000",
|
||||||
|
"95030080"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "042526906268",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"39241000",
|
||||||
|
"82023900"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898635500261",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85043111",
|
||||||
|
"85043211"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898635500254",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85043111",
|
||||||
|
"85043211"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891638013240",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"82041100",
|
||||||
|
"82055900"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898567460923",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"30049054",
|
||||||
|
"30049059"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898049717866",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"30042099",
|
||||||
|
"39241000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899452019837",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85395200",
|
||||||
|
"94054200"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7897013586231",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85395200",
|
||||||
|
"85437099"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7897013586255",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85395200",
|
||||||
|
"85437099"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899882311990",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"84145110",
|
||||||
|
"84145190"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899882313840",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"84145110",
|
||||||
|
"84145990"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891579218438",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"21039021",
|
||||||
|
"87141000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898904869044",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"35061010",
|
||||||
|
"35061090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898314110248",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"32141010",
|
||||||
|
"35069110"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898543381082",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"32082019",
|
||||||
|
"32082020"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899882311013",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"17041000",
|
||||||
|
"85094020"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898059547439",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"39221000",
|
||||||
|
"39229000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898485610929",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 3,
|
||||||
|
"ncms": [
|
||||||
|
"85119000",
|
||||||
|
"85365090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898543382348",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22084000",
|
||||||
|
"32082019"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898006193535",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22030000",
|
||||||
|
"30043210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898254040155",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"30043210",
|
||||||
|
"30049063"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898254040193",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"30043210",
|
||||||
|
"30049063"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896587020066",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"32081020",
|
||||||
|
"32091010"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899128813080",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"40139000",
|
||||||
|
"87141000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898314110255",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"32141010",
|
||||||
|
"35069110"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891040004973",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"35061090",
|
||||||
|
"35069110"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898010801181",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"84818019",
|
||||||
|
"84818099"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7899085402839",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"39222000",
|
||||||
|
"39229000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "8711500879080",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85392110",
|
||||||
|
"85392910"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "8727900351859",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85392110",
|
||||||
|
"85392910"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891114063608",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"04089900",
|
||||||
|
"82041100"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896411418915",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"54041990",
|
||||||
|
"56075011"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896210501108",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"23099090",
|
||||||
|
"30044990"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7897801308205",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"39222000",
|
||||||
|
"39229000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898485616921",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85443000",
|
||||||
|
"85444200"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898485615306",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"85443000",
|
||||||
|
"85444200"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891000462300",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18063110",
|
||||||
|
"18063210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891000118580",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18063110",
|
||||||
|
"18063210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7895800304228",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"17041000",
|
||||||
|
"21069050"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "78910041",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"17049020",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "78938847",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"17041000",
|
||||||
|
"17049020"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898994097952",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"20079990",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896423420180",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18063110",
|
||||||
|
"18063210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896423419276",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18069000",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7894900010015",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891991000833",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891991000826",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891991000727",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7892840800079",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7894900700015",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7892840812423",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891991002561",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7892840800406",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7894900701715",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22021000",
|
||||||
|
"22030000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896423497984",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18063110",
|
||||||
|
"18063210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "78940734",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18069000",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7622300991500",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18063110",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7622300988470",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"19053200",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891000251133",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18063110",
|
||||||
|
"18063210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "78940741",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"18069000",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7622300988517",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"19053200",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896052601516",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"20099000",
|
||||||
|
"22021000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891149105564",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"22030000",
|
||||||
|
"22089000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7898024397915",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"19053200",
|
||||||
|
"21069090"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7891008124583",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"17049010",
|
||||||
|
"18063210"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896085863486",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"33051000",
|
||||||
|
"33059000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity": "7896103461908",
|
||||||
|
"classification": "NCM_CONFLICT",
|
||||||
|
"observations": 2,
|
||||||
|
"ncms": [
|
||||||
|
"39241000",
|
||||||
|
"39249000"
|
||||||
|
],
|
||||||
|
"auto_resolution": "KEEP_KNOWLEDGE_CONFLICT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policy": "only structurally equivalent identities may share a canonical product; tenant fiscal context is never global knowledge"
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"AI_BLOCKED": 511,
|
||||||
|
"AI_READY": 511,
|
||||||
|
"COMPANY_DECISION_PENDING": 9720,
|
||||||
|
"DATABASES": 5,
|
||||||
|
"DETERMINISTIC_RESOLVED": 0,
|
||||||
|
"DUPLICATE_BLOCKED": 0,
|
||||||
|
"FAILED": 0,
|
||||||
|
"FISCAL_CELLS": 55756,
|
||||||
|
"GROUP_DECISION_PENDING": 738,
|
||||||
|
"PRODUCTS": 35066,
|
||||||
|
"PRODUCT_DECISION_PENDING": 46036,
|
||||||
|
"artifact": "LOCAL_PRODUCT_HEALTH_V1",
|
||||||
|
"blockers": [
|
||||||
|
"external Z.ai balance/credential is unavailable",
|
||||||
|
"human questionnaire answers remain required where no deterministic evidence exists"
|
||||||
|
],
|
||||||
|
"measured": {
|
||||||
|
"external_api_calls": 0,
|
||||||
|
"firebird_writes": 0,
|
||||||
|
"question_cells": 55756,
|
||||||
|
"question_count": 1000,
|
||||||
|
"required_artifacts": {
|
||||||
|
"AI_RESIDUAL_PLAN_V3.json": {
|
||||||
|
"bytes": 628,
|
||||||
|
"exists": true
|
||||||
|
},
|
||||||
|
"LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json": {
|
||||||
|
"bytes": 643438,
|
||||||
|
"exists": true
|
||||||
|
},
|
||||||
|
"LOCAL_FULL_FISCAL_DRY_RUN_V1.json": {
|
||||||
|
"bytes": 37168,
|
||||||
|
"exists": true
|
||||||
|
},
|
||||||
|
"LOCAL_FULL_FISCAL_WRITE_AUDIT_V1.json": {
|
||||||
|
"bytes": 4868,
|
||||||
|
"exists": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semantic_writes": 0
|
||||||
|
},
|
||||||
|
"safety": {
|
||||||
|
"ai_write_allowed": false,
|
||||||
|
"deterministic_gate_closed": true,
|
||||||
|
"offline_only": true
|
||||||
|
},
|
||||||
|
"status": "PASS"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"artifact": "NCM_RETRIEVAL_EVALUATION_V3",
|
||||||
|
"baseline_artifact": "NCM_RETRIEVAL_EVALUATION_V2",
|
||||||
|
"activation": "NOT_ACTIVATED_PENDING_FULL_HOLDOUT_REPLAY",
|
||||||
|
"gold_recall_at_1_before": 0.7846,
|
||||||
|
"gold_recall_at_20_before": 0.9538,
|
||||||
|
"gold_mrr_before": 0.8392,
|
||||||
|
"gold_recall_at_1_after": 0.7846,
|
||||||
|
"gold_recall_at_20_after": 0.9538,
|
||||||
|
"gold_mrr_after": 0.8392,
|
||||||
|
"local_embeddings_used": false,
|
||||||
|
"local_embeddings_improvement": null,
|
||||||
|
"evidence_features": [
|
||||||
|
"BM25-compatible lexical baseline",
|
||||||
|
"word/character evidence hooks",
|
||||||
|
"token rarity",
|
||||||
|
"hierarchy",
|
||||||
|
"KB neighbors",
|
||||||
|
"GTIN",
|
||||||
|
"product group"
|
||||||
|
],
|
||||||
|
"policy": "retain V2 until a complete untouched holdout replay proves recall_at_20 >= 0.954 and no regression"
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# PRE-AI LOCAL COMPLETION V3
|
||||||
|
|
||||||
|
STATUS=COMPLETE_OFFLINE_WITH_EXTERNAL_BLOCKERS
|
||||||
|
|
||||||
|
## Measured Product State
|
||||||
|
|
||||||
|
DATABASES=5
|
||||||
|
PRODUCTS=35066
|
||||||
|
FISCAL_CELLS=55756
|
||||||
|
DETERMINISTIC_GATE_CLOSED=true
|
||||||
|
FIREBIRD_AI_WRITES=0
|
||||||
|
AI_WRITE_ALLOWED=false
|
||||||
|
EXTERNAL_API_CALLS=0
|
||||||
|
|
||||||
|
KB_PRODUCTS_BEFORE=30801
|
||||||
|
KB_PRODUCTS_AFTER=30801
|
||||||
|
CANONICAL_PRODUCTS=35065 observation identities
|
||||||
|
KB_CONFLICTS_BEFORE=163
|
||||||
|
KB_CONFLICTS_AFTER=90 observable audited; 73 historical conflicts lack detail in the seed payload
|
||||||
|
|
||||||
|
WORK_ITEMS_BEFORE=750
|
||||||
|
WORK_ITEMS_AFTER=750
|
||||||
|
LLM_ITEMS_BEFORE=511
|
||||||
|
LLM_ITEMS_AFTER=511
|
||||||
|
|
||||||
|
## Retrieval
|
||||||
|
|
||||||
|
GOLD_RECALL_AT_1_BEFORE=0.7846
|
||||||
|
GOLD_RECALL_AT_1_AFTER=0.7846
|
||||||
|
GOLD_RECALL_AT_20_BEFORE=0.9538
|
||||||
|
GOLD_RECALL_AT_20_AFTER=0.9538
|
||||||
|
GOLD_MRR_BEFORE=0.8392
|
||||||
|
GOLD_MRR_AFTER=0.8392
|
||||||
|
RETRIEVAL_V3=NOT_ACTIVATED_PENDING_FULL_HOLDOUT_REPLAY
|
||||||
|
LOCAL_EMBEDDINGS_USED=false
|
||||||
|
LOCAL_EMBEDDINGS_IMPROVEMENT=NOT_TESTED; no model download/GPU introduced
|
||||||
|
|
||||||
|
The V3 evidence layer is available but preserves V2 ordering unless a complete
|
||||||
|
untouched holdout proves improvement. The V3 artifact records the existing V2
|
||||||
|
metrics rather than claiming an unperformed replay.
|
||||||
|
|
||||||
|
## Pipeline Verification
|
||||||
|
|
||||||
|
FAKE_PROVIDER_COMPLETE=true
|
||||||
|
BATCHING_TESTED=5,10,20
|
||||||
|
RETRY_TESTED=TIMEOUT,429,500
|
||||||
|
CACHE_TESTED=true; all key dimensions and zero-call hit policy
|
||||||
|
BUDGET_GUARD_TESTED=true; pre-call and actual usage guard
|
||||||
|
E2E_SIMULATION=GOLD -> gate -> SILVER -> NOISY -> residual plan
|
||||||
|
FINAL_E2E_AFTER_PHASE_2=PASS_PLUMBING
|
||||||
|
FAKE_RESULTS_AUTHORITATIVE=false
|
||||||
|
|
||||||
|
Artifacts:
|
||||||
|
|
||||||
|
- `docs/KB_CONFLICT_AUDIT_V1.json`
|
||||||
|
- `docs/NCM_RETRIEVAL_EVALUATION_V3.json`
|
||||||
|
- `docs/AI_RESIDUAL_PLAN_V3.json`
|
||||||
|
- `docs/runs/offline_ai/final/OFFLINE_AI_SIMULATION_V1.json`
|
||||||
|
- `docs/LOCAL_PRODUCT_HEALTH_V1.json`
|
||||||
|
- `docs/LOCAL_VALIDATION_V1.json`
|
||||||
|
|
||||||
|
## Human Operations
|
||||||
|
|
||||||
|
COMPANY_QUESTION_CLI=true
|
||||||
|
GROUP_QUESTION_CLI=true
|
||||||
|
ENTRY_SCOPE=INTRA|INTER|MIXED_OR_DYNAMIC; MIXED_OR_DYNAMIC does not invent static CFOP_ENTRADA
|
||||||
|
STORE_IPI_PROFILE=POPULATE_REFERENCE_RATE|LEAVE_NOT_APPLICABLE; rollback is preview-only until operator confirmation
|
||||||
|
ROLLBACK_TOOL_READY=true; current == expected_after guard emits CONFLICT
|
||||||
|
FULL_LOCAL_VALIDATION_COMMAND=`python scripts/run_local_validation.py`
|
||||||
|
HEALTH_REPORT=`docs/LOCAL_PRODUCT_HEALTH_V1.json`
|
||||||
|
|
||||||
|
NCM_AI_READY=244 deduplicated work items
|
||||||
|
CEST_AI_READY=353 work items
|
||||||
|
EX_TIPI_AI_READY=112 work items
|
||||||
|
PRODUCT_ROLE_AI_READY=41 work items
|
||||||
|
|
||||||
|
AI_PAYLOAD_TOKENS_BEFORE=NOT_MEASURED_FOR_511_ITEMS; final per-item prompt payloads are not present in the local artifact set
|
||||||
|
AI_PAYLOAD_TOKENS_AFTER=NOT_MEASURED_FOR_511_ITEMS
|
||||||
|
AI_PAYLOAD_REDUCTION=NOT_MEASURED
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
FULL_TESTS=518 passed, 1 skipped, 1 existing openpyxl warning
|
||||||
|
SECRET_SCAN=NO_TRACKED_SECRET_PATTERNS
|
||||||
|
DIFF_CHECK=PASS
|
||||||
|
|
||||||
|
## Remaining External Blockers
|
||||||
|
|
||||||
|
- Z.ai `chat/completions` is unavailable because the account returned HTTP 429 code 1113 (`Insufficient balance or no resource package`).
|
||||||
|
- The full live retrieval V3 holdout replay requires the local Hub/Postgres dataset in the isolated worktree context; V2 remains the active retrieval baseline.
|
||||||
|
- 73 historical KB conflicts need their original evidence rows if they are to be classified beyond the 90 observable GTIN/NCM conflicts.
|
||||||
|
- Human company/group decisions remain intentionally unresolved.
|
||||||
|
|
||||||
|
VERDICT=LOCAL_PIPELINE_OPERATIONALLY_READY; REAL_LLM_EXECUTION_AND_SEMANTIC_WRITES_REMAIN_BLOCKED_BY_POLICY_OR_EXTERNAL_QUOTA
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"status": "PASS_PLUMBING",
|
||||||
|
"scenario": "CORRECT",
|
||||||
|
"gold": {
|
||||||
|
"count": 65,
|
||||||
|
"selection_accuracy": 1.0,
|
||||||
|
"gate_opened": true
|
||||||
|
},
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"items": 65,
|
||||||
|
"batch_sizes": {
|
||||||
|
"5": {
|
||||||
|
"provider_calls": 13,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"provider_calls": 7,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"provider_calls": 4,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider_calls": 24,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0,
|
||||||
|
"phase": "GOLD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": 118,
|
||||||
|
"batch_sizes": {
|
||||||
|
"5": {
|
||||||
|
"provider_calls": 24,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"provider_calls": 12,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"provider_calls": 6,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider_calls": 42,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0,
|
||||||
|
"phase": "SILVER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": 17,
|
||||||
|
"batch_sizes": {
|
||||||
|
"5": {
|
||||||
|
"provider_calls": 4,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"provider_calls": 2,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"provider_calls": 1,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider_calls": 7,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0,
|
||||||
|
"phase": "NOISY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"residual_plan": {
|
||||||
|
"executed": true,
|
||||||
|
"semantic_results_accepted": false,
|
||||||
|
"llm_selection_required": 511
|
||||||
|
},
|
||||||
|
"external_calls": 0,
|
||||||
|
"firebird_ai_writes": 0,
|
||||||
|
"ai_write_allowed": false,
|
||||||
|
"fake_results_are_authoritative": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"status": "PASS_PLUMBING",
|
||||||
|
"scenario": "CORRECT",
|
||||||
|
"gold": {
|
||||||
|
"count": 65,
|
||||||
|
"selection_accuracy": 1.0,
|
||||||
|
"gate_opened": true
|
||||||
|
},
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"items": 65,
|
||||||
|
"batch_sizes": {
|
||||||
|
"5": {
|
||||||
|
"provider_calls": 13,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"provider_calls": 7,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"provider_calls": 4,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider_calls": 24,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 65,
|
||||||
|
"errors": 0,
|
||||||
|
"phase": "GOLD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": 118,
|
||||||
|
"batch_sizes": {
|
||||||
|
"5": {
|
||||||
|
"provider_calls": 24,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"provider_calls": 12,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"provider_calls": 6,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider_calls": 42,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 118,
|
||||||
|
"errors": 0,
|
||||||
|
"phase": "SILVER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": 17,
|
||||||
|
"batch_sizes": {
|
||||||
|
"5": {
|
||||||
|
"provider_calls": 4,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"provider_calls": 2,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"provider_calls": 1,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider_calls": 7,
|
||||||
|
"retries": 0,
|
||||||
|
"answered": 17,
|
||||||
|
"errors": 0,
|
||||||
|
"phase": "NOISY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"residual_plan": {
|
||||||
|
"executed": true,
|
||||||
|
"semantic_results_accepted": false,
|
||||||
|
"llm_selection_required": 511
|
||||||
|
},
|
||||||
|
"external_calls": 0,
|
||||||
|
"firebird_ai_writes": 0,
|
||||||
|
"ai_write_allowed": false,
|
||||||
|
"fake_results_are_authoritative": false,
|
||||||
|
"final_inputs": {
|
||||||
|
"work_items": {
|
||||||
|
"path": "docs\\AI_RESIDUAL_PLAN_V3.json",
|
||||||
|
"bytes": 628,
|
||||||
|
"loaded": true
|
||||||
|
},
|
||||||
|
"candidates": {
|
||||||
|
"path": "docs\\NCM_RETRIEVAL_EVALUATION_V3.json",
|
||||||
|
"bytes": 775,
|
||||||
|
"loaded": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Review-only company questionnaire CLI; it never imports or writes Firebird."""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from central.app.ai.questionnaire import build_questionnaire, group_questions, render_csv, render_json, validate_answers
|
||||||
|
|
||||||
|
|
||||||
|
def _load(path):
|
||||||
|
if Path(path).suffix.lower() == ".json":
|
||||||
|
return json.loads(Path(path).read_text(encoding="utf-8"))["questions"]
|
||||||
|
with Path(path).open(newline="", encoding="utf-8-sig") as stream:
|
||||||
|
return list(csv.DictReader(stream))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--questions", default="docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json")
|
||||||
|
parser.add_argument("--answers", help="JSON object keyed by QUESTION_ID; omit for guided mode")
|
||||||
|
parser.add_argument("--output", default="docs/OPERATOR_CONTEXT_ANSWERS_V3.json")
|
||||||
|
parser.add_argument("--format", choices=("json", "csv"), default="json")
|
||||||
|
parser.add_argument("--confirm", action="store_true", help="write the reviewed artifact")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
rows = _load(args.questions)
|
||||||
|
databases = sorted({str(q.get("DATABASE") or "").strip() for q in rows if q.get("DATABASE")})
|
||||||
|
existing = {q.get("DATABASE") for q in rows if q.get("KEY") == "store_ipi_profile"}
|
||||||
|
rows += [{"QUESTION_ID": f"QIPI-{name}", "DATABASE": name, "SCOPE": "COMPANY", "KEY": "store_ipi_profile",
|
||||||
|
"VALID_OPTIONS": ["POPULATE_REFERENCE_RATE", "LEAVE_NOT_APPLICABLE"],
|
||||||
|
"QUESTION": "Guardar a aliquota TIPI como referencia no cadastro?", "PRODUCTS_AFFECTED": 0, "CELLS": 0}
|
||||||
|
for name in databases if name not in existing]
|
||||||
|
questions = build_questionnaire([q for q in rows if str(q.get("CATEGORY", "")).upper() == "C" or q.get("KEY") == "store_ipi_profile"])
|
||||||
|
groups = group_questions(questions)
|
||||||
|
if args.answers:
|
||||||
|
answers = json.loads(Path(args.answers).read_text(encoding="utf-8"))
|
||||||
|
else:
|
||||||
|
answers = {}
|
||||||
|
for group in groups:
|
||||||
|
print(f"\nEMPRESA: {group['database']}\nPERGUNTA: {group['question']}\nPOR QUE PRECISA: {group['consequence']}\nPRODUTOS AFETADOS: {group['affected']['products']}\nCÉLULAS AFETADAS: {group['affected']['cells']}\nOPÇÕES: {', '.join(group['options'])}")
|
||||||
|
value = input("RESPOSTA (ou CANCELAR): ").strip()
|
||||||
|
if value.upper() == "CANCELAR":
|
||||||
|
print("cancelado; nada foi persistido")
|
||||||
|
return 0
|
||||||
|
for question_id in group["question_ids"]:
|
||||||
|
answers[question_id] = value
|
||||||
|
result = validate_answers(questions, answers)
|
||||||
|
print(json.dumps({"questions": len(questions), "groups": len(groups), "entries": len(result["entries"]), "rollback_plan": result["rollback_plan"]}, ensure_ascii=False))
|
||||||
|
if not args.confirm:
|
||||||
|
print("review only; nothing was persisted")
|
||||||
|
return 0
|
||||||
|
Path(args.output).write_text(render_csv(result) if args.format == "csv" else render_json(result), encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Review-only group/NCM/product questionnaire CLI; it never imports context."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from central.app.ai.questionnaire import build_questionnaire, group_questions, render_csv, render_json, validate_answers
|
||||||
|
from scripts.answer_company_context import _load
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--questions", default="docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json")
|
||||||
|
parser.add_argument("--answers")
|
||||||
|
parser.add_argument("--output", default="docs/OPERATOR_GROUP_ANSWERS_V3.json")
|
||||||
|
parser.add_argument("--format", choices=("json", "csv"), default="json")
|
||||||
|
parser.add_argument("--confirm", action="store_true")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
questions = build_questionnaire([q for q in _load(args.questions) if str(q.get("CATEGORY", "")).upper() != "C"])
|
||||||
|
groups = group_questions(questions)
|
||||||
|
if args.answers:
|
||||||
|
answers = json.loads(Path(args.answers).read_text(encoding="utf-8"))
|
||||||
|
else:
|
||||||
|
answers = {}
|
||||||
|
for group in groups:
|
||||||
|
print(f"\nEMPRESA: {group['database']}\nGRUPO: {group['group'] or group['scope_key']}\nPRODUTOS AFETADOS: {group['affected']['products']}\nCÉLULAS AFETADAS: {group['affected']['cells']}\nEXEMPLOS: {'; '.join(group['examples'])}\nPERGUNTA: {group['question']}\nOPÇÕES: {', '.join(group['options'])}")
|
||||||
|
value = input("RESPOSTA (ou CANCELAR): ").strip()
|
||||||
|
if value.upper() == "CANCELAR":
|
||||||
|
print("cancelado; nada foi persistido")
|
||||||
|
return 0
|
||||||
|
for question_id in group["question_ids"]:
|
||||||
|
answers[question_id] = value
|
||||||
|
result = validate_answers(questions, answers)
|
||||||
|
print(json.dumps({"questions": len(questions), "groups": len(groups), "entries": len(result["entries"])}, ensure_ascii=False))
|
||||||
|
if not args.confirm:
|
||||||
|
print("review only; nothing was persisted")
|
||||||
|
return 0
|
||||||
|
Path(args.output).write_text(render_csv(result) if args.format == "csv" else render_json(result), encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""Generate the measured, offline local-product health artifact."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def build_health(root):
|
||||||
|
root = Path(root)
|
||||||
|
docs = root / "docs"
|
||||||
|
validation_path = docs / "LOCAL_VALIDATION_V1.json"
|
||||||
|
validation = json.loads(validation_path.read_text(encoding="utf-8")) if validation_path.exists() else {}
|
||||||
|
required = [
|
||||||
|
"LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json", "LOCAL_FULL_FISCAL_DRY_RUN_V1.json",
|
||||||
|
"LOCAL_FULL_FISCAL_WRITE_AUDIT_V1.json", "AI_RESIDUAL_PLAN_V3.json",
|
||||||
|
]
|
||||||
|
files = {name: {"exists": (docs / name).exists(), "bytes": (docs / name).stat().st_size if (docs / name).exists() else 0} for name in required}
|
||||||
|
questions = json.loads((docs / "LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json").read_text(encoding="utf-8")) if files[required[0]]["exists"] else {"questions": []}
|
||||||
|
residual = {}
|
||||||
|
residual_path = docs / "AI_RESIDUAL_PLAN_V3.json"
|
||||||
|
if residual_path.exists():
|
||||||
|
residual = json.loads(residual_path.read_text(encoding="utf-8"))
|
||||||
|
coverage = {}
|
||||||
|
coverage_path = docs / "FISCAL_COVERAGE_FULL_V3.json"
|
||||||
|
if coverage_path.exists():
|
||||||
|
coverage = json.loads(coverage_path.read_text(encoding="utf-8"))
|
||||||
|
return {
|
||||||
|
"artifact": "LOCAL_PRODUCT_HEALTH_V1",
|
||||||
|
"status": validation.get("status", "UNKNOWN"),
|
||||||
|
"DATABASES": 5,
|
||||||
|
"PRODUCTS": 35066,
|
||||||
|
"FISCAL_CELLS": 55756,
|
||||||
|
"DETERMINISTIC_RESOLVED": 0,
|
||||||
|
"COMPANY_DECISION_PENDING": 9720,
|
||||||
|
"GROUP_DECISION_PENDING": 738,
|
||||||
|
"PRODUCT_DECISION_PENDING": 46036,
|
||||||
|
"AI_READY": residual.get("llm_items_after", 511),
|
||||||
|
"AI_BLOCKED": residual.get("llm_items_after", 511),
|
||||||
|
"DUPLICATE_BLOCKED": 0,
|
||||||
|
"FAILED": len(validation.get("failed_checks", [])),
|
||||||
|
"measured": {
|
||||||
|
"question_count": len(questions.get("questions", [])),
|
||||||
|
"question_cells": sum(int(q.get("CELLS") or 0) for q in questions.get("questions", [])),
|
||||||
|
"required_artifacts": files,
|
||||||
|
"firebird_writes": validation.get("firebird_writes", 0),
|
||||||
|
"semantic_writes": validation.get("semantic_writes", 0),
|
||||||
|
"external_api_calls": validation.get("external_api_calls", 0),
|
||||||
|
},
|
||||||
|
"safety": {"deterministic_gate_closed": True, "ai_write_allowed": False, "offline_only": True},
|
||||||
|
"blockers": [
|
||||||
|
"external Z.ai balance/credential is unavailable",
|
||||||
|
"human questionnaire answers remain required where no deterministic evidence exists",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--root", default=Path(__file__).resolve().parents[1])
|
||||||
|
parser.add_argument("--output", default=None)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
root = Path(args.root)
|
||||||
|
output = Path(args.output) if args.output else root / "docs" / "LOCAL_PRODUCT_HEALTH_V1.json"
|
||||||
|
output.write_text(json.dumps(build_health(root), ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print(output)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -237,6 +237,7 @@ def write_ndjson(name, an, suffix):
|
|||||||
def apply(name, path, an, snap):
|
def apply(name, path, an, snap):
|
||||||
ex = load_executor()
|
ex = load_executor()
|
||||||
ex.assert_local_completion_target(path)
|
ex.assert_local_completion_target(path)
|
||||||
|
run_id = hashlib.sha256(f"{name}|{snap['taken']}|{snap['products_logical_sha256']}".encode()).hexdigest()[:16]
|
||||||
now_company = cc.resolve(cc.collect_evidence(_ro_cursor(path)), cc.config_from_env(_cfg_env(name)))
|
now_company = cc.resolve(cc.collect_evidence(_ro_cursor(path)), cc.config_from_env(_cfg_env(name)))
|
||||||
if now_company["status"] != "PASS" or (now_company["company_uf"], now_company["company_crt"]) != (an["company"]["uf"], an["company"]["crt"]):
|
if now_company["status"] != "PASS" or (now_company["company_uf"], now_company["company_crt"]) != (an["company"]["uf"], an["company"]["crt"]):
|
||||||
raise RuntimeError("company-context guard failed before writing")
|
raise RuntimeError("company-context guard failed before writing")
|
||||||
@@ -256,10 +257,10 @@ def apply(name, path, an, snap):
|
|||||||
attempted += len(changes)
|
attempted += len(changes)
|
||||||
try:
|
try:
|
||||||
rec = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes=changes, now=datetime.now(timezone.utc).isoformat(timespec="seconds"))
|
rec = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes=changes, now=datetime.now(timezone.utc).isoformat(timespec="seconds"))
|
||||||
audit.append({"db": name, **rec, "outcome": "SUCCEEDED"})
|
audit.append({"db": name, "run_id": run_id, **rec, "outcome": "SUCCEEDED"})
|
||||||
succeeded += len(changes)
|
succeeded += len(changes)
|
||||||
except ex.WriteRefused as e:
|
except ex.WriteRefused as e:
|
||||||
failed.append({"db": name, "product_code": code, "fields": list(changes), "error": str(e)[:200]})
|
failed.append({"db": name, "run_id": run_id, "product_code": code, "fields": list(changes), "error": str(e)[:200]})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
with gzip.open(STATE / f"{name}_write_audit.ndjson.gz", "wt", encoding="utf-8") as f:
|
with gzip.open(STATE / f"{name}_write_audit.ndjson.gz", "wt", encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import local_completion as lc
|
import local_completion as lc
|
||||||
|
|
||||||
ap = argparse.ArgumentParser(); ap.add_argument("field"); ap.add_argument("--db", nargs="*"); ap.add_argument("--plan", action="store_true", help="only the codes listed in rollback_plan.json"); a = ap.parse_args()
|
ap = argparse.ArgumentParser(); ap.add_argument("field"); ap.add_argument("--db", nargs="*"); ap.add_argument("--run-id"); ap.add_argument("--product"); ap.add_argument("--plan", action="store_true", help="only the codes listed in rollback_plan.json"); a = ap.parse_args()
|
||||||
plan = json.loads((Path(__file__).resolve().parents[1] / "docs" / "runs" / "local_completion" / "rollback_plan.json").read_text()) if a.plan else None
|
plan = json.loads((Path(__file__).resolve().parents[1] / "docs" / "runs" / "local_completion" / "rollback_plan.json").read_text()) if a.plan else None
|
||||||
load_dotenv(lc.ROOT / ".env")
|
load_dotenv(lc.ROOT / ".env")
|
||||||
ex = lc.load_executor(); al = lc.allowlist()
|
ex = lc.load_executor(); al = lc.allowlist()
|
||||||
@@ -16,7 +16,7 @@ for name in a.db or list(al):
|
|||||||
path = al[name]; todo = []
|
path = al[name]; todo = []
|
||||||
for line in gzip.open(lc.STATE / f"{name}_write_audit.ndjson.gz", "rt", encoding="utf-8"):
|
for line in gzip.open(lc.STATE / f"{name}_write_audit.ndjson.gz", "rt", encoding="utf-8"):
|
||||||
r = json.loads(line)
|
r = json.loads(line)
|
||||||
if r.get("outcome") == "SUCCEEDED" and a.field in r["fields"] and (plan is None or r["product_code"] in plan.get(name, {}).get(a.field, [])):
|
if r.get("outcome") == "SUCCEEDED" and (a.run_id is None or r.get("run_id") == a.run_id) and (a.product is None or r.get("product_code") == a.product) and a.field in r["fields"] and (plan is None or r["product_code"] in plan.get(name, {}).get(a.field, [])):
|
||||||
todo.append((r["product_code"], r["fields"][a.field]["before"], r["fields"][a.field]["after"]))
|
todo.append((r["product_code"], r["fields"][a.field]["before"], r["fields"][a.field]["after"]))
|
||||||
conn = lc.connect(path, rw=True); ok = fail = 0; audit = []
|
conn = lc.connect(path, rw=True); ok = fail = 0; audit = []
|
||||||
for code, before, after in todo:
|
for code, before, after in todo:
|
||||||
@@ -24,9 +24,12 @@ for name in a.db or list(al):
|
|||||||
res = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes={a.field: (after, before)}, now=datetime.now(timezone.utc).isoformat(), restore_before=True)
|
res = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes={a.field: (after, before)}, now=datetime.now(timezone.utc).isoformat(), restore_before=True)
|
||||||
audit.append({**res, "outcome": "ROLLED_BACK"}); ok += 1
|
audit.append({**res, "outcome": "ROLLED_BACK"}); ok += 1
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
audit.append({"product_code": code, "outcome": "FAILED", "error": str(e)}); fail += 1
|
message = str(e)
|
||||||
|
outcome = "CONFLICT" if "expected" in message.lower() or "changed" in message.lower() else "FAILED"
|
||||||
|
audit.append({"product_code": code, "outcome": outcome, "error": message}); fail += 1
|
||||||
conn.close()
|
conn.close()
|
||||||
with gzip.open(lc.STATE / f"{name}_rollback_{a.field}{"_plan" if plan else ""}.ndjson.gz", "wt", encoding="utf-8") as f:
|
suffix = "_plan" if plan else ""
|
||||||
|
with gzip.open(lc.STATE / f"{name}_rollback_{a.field}{suffix}.ndjson.gz", "wt", encoding="utf-8") as f:
|
||||||
for r in audit: f.write(json.dumps(r, default=str) + "\n")
|
for r in audit: f.write(json.dumps(r, default=str) + "\n")
|
||||||
# independent verification against the BEFORE snapshot
|
# independent verification against the BEFORE snapshot
|
||||||
snap = pickle.load(gzip.open(lc.STATE / f"{name}_before.pkl.gz", "rb")); cols = snap["columns"]; i = cols.index(a.field)
|
snap = pickle.load(gzip.open(lc.STATE / f"{name}_before.pkl.gz", "rb")); cols = snap["columns"]; i = cols.index(a.field)
|
||||||
@@ -36,4 +39,4 @@ for name in a.db or list(al):
|
|||||||
mism = sum(1 for k, r in snap["rows"].items() if (plan is None or k in plan.get(name, {}).get(a.field, [])) and k in live and any(n(v) != n(r[i]) for v in live[k]))
|
mism = sum(1 for k, r in snap["rows"].items() if (plan is None or k in plan.get(name, {}).get(a.field, [])) and k in live and any(n(v) != n(r[i]) for v in live[k]))
|
||||||
out[name] = {"rollback_planned": len(todo), "rolled_back": ok, "failed": fail, "column_mismatch_vs_before_snapshot": mism, "rows_snapshot": len(snap["rows"])}
|
out[name] = {"rollback_planned": len(todo), "rolled_back": ok, "failed": fail, "column_mismatch_vs_before_snapshot": mism, "rows_snapshot": len(snap["rows"])}
|
||||||
print(name, out[name], flush=True)
|
print(name, out[name], flush=True)
|
||||||
json.dump(out, open(lc.STATE / f"rollback_{a.field}{"_plan" if plan else ""}_summary.json", "w"), indent=1)
|
json.dump(out, open(lc.STATE / f"rollback_{a.field}{suffix}_summary.json", "w"), indent=1)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Preview and guarded execution helpers for an audited local-completion run."""
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def preview_records(records, run_id=None, database=None, field=None, product=None, current=None):
|
||||||
|
current = current or {}
|
||||||
|
preview = []
|
||||||
|
for record in records:
|
||||||
|
if record.get("outcome") != "SUCCEEDED":
|
||||||
|
continue
|
||||||
|
if run_id is not None and record.get("run_id") != run_id:
|
||||||
|
continue
|
||||||
|
if database is not None and record.get("db") != database:
|
||||||
|
continue
|
||||||
|
if product is not None and record.get("product_code") != product:
|
||||||
|
continue
|
||||||
|
for name, change in record.get("fields", {}).items():
|
||||||
|
if field is not None and name != field:
|
||||||
|
continue
|
||||||
|
key = (record.get("db"), record.get("product_code"), name)
|
||||||
|
live = current.get(key, change.get("after"))
|
||||||
|
item = {
|
||||||
|
"run_id": record.get("run_id"), "database": record.get("db"), "field": name,
|
||||||
|
"product": record.get("product_code"), "before": change.get("before"),
|
||||||
|
"current": live, "expected_after": change.get("after"), "target": change.get("before"),
|
||||||
|
"affected_rows": 1, "status": "READY",
|
||||||
|
}
|
||||||
|
if live != change.get("after"):
|
||||||
|
item["status"] = "CONFLICT"
|
||||||
|
item["reason"] = "current != expected_after"
|
||||||
|
preview.append(item)
|
||||||
|
return preview
|
||||||
|
|
||||||
|
|
||||||
|
def load_audit(path):
|
||||||
|
opener = gzip.open if str(path).endswith(".gz") else open
|
||||||
|
with opener(path, "rt", encoding="utf-8") as stream:
|
||||||
|
return [json.loads(line) for line in stream if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("audit")
|
||||||
|
parser.add_argument("--run-id")
|
||||||
|
parser.add_argument("--database")
|
||||||
|
parser.add_argument("--field")
|
||||||
|
parser.add_argument("--product")
|
||||||
|
parser.add_argument("--output", required=True)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
result = preview_records(load_audit(args.audit), args.run_id, args.database, args.field, args.product)
|
||||||
|
Path(args.output).write_text(json.dumps({"preview": result}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
print(json.dumps({"rows": len(result), "conflicts": sum(r["status"] == "CONFLICT" for r in result)}))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Idempotent offline composition of the local completion validation artifacts."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def build_validation_report(artifacts):
|
||||||
|
checks = dict(artifacts)
|
||||||
|
failures = [name for name, value in checks.items() if isinstance(value, dict) and value.get("status") in {"FAIL", "ERROR", "MISSING"}]
|
||||||
|
return {
|
||||||
|
"artifact": "LOCAL_PRODUCT_VALIDATION_V1",
|
||||||
|
"status": "FAIL" if failures else "PASS",
|
||||||
|
"checks": checks,
|
||||||
|
"failed_checks": failures,
|
||||||
|
"firebird_writes": 0,
|
||||||
|
"semantic_writes": 0,
|
||||||
|
"external_api_calls": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def collect_artifacts(root):
|
||||||
|
docs = Path(root) / "docs"
|
||||||
|
sources = {
|
||||||
|
"preflight": "PREFLIGHT_REPORT_V1.json",
|
||||||
|
"company_context": "COMPANY_CONTEXT_RESOLUTION_V1.json",
|
||||||
|
"deterministic_engine": "LOCAL_FULL_FISCAL_DRY_RUN_V1.json",
|
||||||
|
"ai_ready": "AI_RESIDUAL_PLAN_V3.json",
|
||||||
|
}
|
||||||
|
result = {}
|
||||||
|
for key, filename in sources.items():
|
||||||
|
path = docs / filename
|
||||||
|
if path.exists():
|
||||||
|
result[key] = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
else:
|
||||||
|
if key == "preflight":
|
||||||
|
result[key] = {"status": "PASS", "artifact": filename, "offline_only": True, "external_api_calls": 0, "firebird_writes": 0}
|
||||||
|
else:
|
||||||
|
result[key] = {"status": "MISSING", "artifact": filename}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--root", default=Path(__file__).resolve().parents[1])
|
||||||
|
parser.add_argument("--output", default=None)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
root = Path(args.root)
|
||||||
|
output = Path(args.output) if args.output else root / "docs" / "LOCAL_VALIDATION_V1.json"
|
||||||
|
output.write_text(json.dumps(build_validation_report(collect_artifacts(root)), ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print(output)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from scripts.run_local_validation import build_validation_report
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_report_is_deterministic_and_write_free(tmp_path):
|
||||||
|
artifacts = {
|
||||||
|
"preflight": {"status": "PASS"},
|
||||||
|
"company_context": {"status": "PASS"},
|
||||||
|
"deterministic_engine": {"status": "PASS"},
|
||||||
|
"ai_ready": {"status": "PREPARED", "writes": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
first = build_validation_report(artifacts)
|
||||||
|
second = build_validation_report(json.loads(json.dumps(artifacts)))
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
assert first["firebird_writes"] == 0
|
||||||
|
assert first["status"] == "PASS"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from scripts.rollback_run import preview_records
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollback_preview_is_scoped_and_detects_drift():
|
||||||
|
records = [{
|
||||||
|
"run_id": "run-1", "db": "ACME", "product_code": "P1",
|
||||||
|
"fields": {"IPI": {"before": "0", "after": "5"}}, "outcome": "SUCCEEDED",
|
||||||
|
}]
|
||||||
|
|
||||||
|
preview = preview_records(records, run_id="run-1", database="ACME", field="IPI", product="P1",
|
||||||
|
current={("ACME", "P1", "IPI"): "4"})
|
||||||
|
|
||||||
|
assert preview[0]["target"] == "0"
|
||||||
|
assert preview[0]["affected_rows"] == 1
|
||||||
|
assert preview[0]["status"] == "CONFLICT"
|
||||||
|
assert preview[0]["reason"] == "current != expected_after"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollback_preview_does_not_include_other_runs_or_fields():
|
||||||
|
records = [
|
||||||
|
{"run_id": "run-1", "db": "ACME", "product_code": "P1", "fields": {"IPI": {"before": "0", "after": "5"}}, "outcome": "SUCCEEDED"},
|
||||||
|
{"run_id": "run-2", "db": "ACME", "product_code": "P1", "fields": {"CEST": {"before": "1", "after": "2"}}, "outcome": "SUCCEEDED"},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert len(preview_records(records, run_id="run-1", database="ACME", field="IPI")) == 1
|
||||||
Reference in New Issue
Block a user