Repository navigation
fix(kb): use bounded review feedback on foundation resumes #316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,24 @@ def offered_ranges(items): | |
| return ranges | ||
|
|
||
|
|
||
| def range_is_offered(ranges, start, end): | ||
| """Adjacent chunks are continuous evidence; a missing line stays a gap. | ||
|
|
||
| Keep stored intervals unchanged so historical native input hashes replay. | ||
| """ | ||
| if isinstance(ranges, int): | ||
| ranges = [(1, ranges)] | ||
| cursor = start | ||
| for left, right in sorted(ranges): | ||
| if left > cursor: | ||
| return False | ||
| if right >= cursor: | ||
| cursor = right + 1 | ||
| if cursor > end: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def preferred_sources(stage, tree, feature_id, paths, limit): | ||
| """New catalog entries use complete, verified discovery ranges before prefixes.""" | ||
| from .feature_discovery_index import language_for, validate_evidence | ||
|
|
@@ -114,18 +132,22 @@ def call_json(self, role, **kwargs): | |
|
|
||
|
|
||
| def _worker(stage, job): | ||
| from .init_knowledge import SYSTEM_KNOWLEDGE, knowledge_prompt, validate_sections | ||
| from .init_knowledge import knowledge_prompt, validate_sections | ||
| from .init_knowledge_inputs import foundation_evidence, knowledge_system | ||
| from .init_support import generate | ||
| from .init_stages import _one_line | ||
| worker = copy.copy(stage) | ||
| worker.head = dict(stage.head) | ||
| worker.rt = copy.copy(stage.rt) | ||
| worker.rt.gateway = _CaptureGateway(stage.rt.gateway) | ||
| worker.record = InitRecord(stage=stage.STAGE, repo=stage.record.repo, pin=stage.record.pin) | ||
| worker._foundation_payload = job["payload"] | ||
| artifacts = [] | ||
| error = "" | ||
| try: | ||
| data = generate(worker.rt, worker.budget, worker.lifecycle.init, system=SYSTEM_KNOWLEDGE, | ||
| if job["payload"].get("foundation_prompt_version") == 4: | ||
| foundation_evidence(worker, job["payload"]) | ||
| data = generate(worker.rt, worker.budget, worker.lifecycle.init, system=knowledge_system(job["payload"]), | ||
| prompt=knowledge_prompt(job["payload"]), validate=validate_sections).data | ||
| for section in data["sections"]: | ||
| if section["facet"] not in job["requested"]: | ||
|
|
@@ -168,6 +190,12 @@ def _validate_result(stage, result): | |
| if (result["requested"] != result["payload"]["facets"] | ||
| or _hash(result["offered"]) != _hash(offered_ranges(result["payload"]["files"] + result["payload"]["docs"]))): | ||
| raise InitError("foundation shown input or requested facets differ from generator payload") | ||
| from .init_knowledge_inputs import foundation_evidence, knowledge_system | ||
| knowledge_system(result["payload"]) | ||
| if result["payload"].get("foundation_prompt_version") == 4: | ||
| if not stage.rt.unlimited_subscription: | ||
| raise InitError("foundation full packet requires unlimited subscription") | ||
| foundation_evidence(stage, result["payload"]) | ||
| for artifact in result["artifacts"]: | ||
| facet, owner = artifact["facet"], result["owner"] | ||
| if (artifact["owner"] != owner or result["payload"]["owner"] != owner | ||
|
|
@@ -190,8 +218,7 @@ def _validate_result(stage, result): | |
| if stage._render_section(artifact["section"], entries) != artifact["text"]: | ||
| raise InitError("foundation generated section reconstruction mismatch") | ||
| for entry in artifact["section"]["evidence"]: | ||
| if not any(start <= entry["start"] <= entry["end"] <= end | ||
| for start, end in result["offered"].get(entry["path"], [])): | ||
| if not range_is_offered(result["offered"].get(entry["path"], []), entry["start"], entry["end"]): | ||
| raise InitError("foundation cached citation outside shown interval") | ||
| if [(e.path, e.start, e.end) for e in entries] != [ | ||
| (e["path"], e["start"], e["end"]) for e in artifact["section"]["evidence"]]: | ||
|
|
@@ -205,8 +232,9 @@ def _validate_result(stage, result): | |
| def _validate_native(stage, artifact, result): | ||
| from ..trace_store import TraceStore | ||
| from .judge_tuning import JUDGE_SYSTEM | ||
| from .init_knowledge import SYSTEM_KNOWLEDGE, knowledge_prompt | ||
| from .init_stages import _one_line, _page_frontmatter | ||
| from .init_knowledge import knowledge_prompt | ||
| from .init_knowledge_inputs import foundation_evidence, knowledge_system | ||
| from .init_stages import _Stage, _one_line, _page_frontmatter | ||
| from .models import parse_json_object | ||
| store = TraceStore(stage.rt.state_dir / "init" / "traces") | ||
| receipt = artifact["judge_receipt"] | ||
|
|
@@ -234,7 +262,7 @@ def _validate_native(stage, artifact, result): | |
| and artifact["section"] in generated["sections"] | ||
| and artifact["generator_title"] == generated.get("title") | ||
| and artifact["title"] == (_one_line(generated.get("title")) or f"{artifact['owner']} knowledge") | ||
| and store.blob(generator["inputs"]["system"]) == SYSTEM_KNOWLEDGE | ||
| and store.blob(generator["inputs"]["system"]) == knowledge_system(result["payload"]) | ||
| and not generator.get("error") and generator["model"]["role"] == "generator" | ||
| and generator["model"]["provider"] == stage.rt.generator.provider | ||
| and generator["model"]["model"] == stage.rt.generator.model | ||
|
|
@@ -249,7 +277,9 @@ def _validate_native(stage, artifact, result): | |
| and payload["change"]["page"] == artifact["page"] | ||
| and payload["change"]["after"] == _page_frontmatter(artifact["title"], kind="architecture", | ||
| today=artifact["approved_today"], tags=stage.tags) + "\n" + artifact["text"] | ||
| and payload["evidence"] == stage._judge_evidence([Evidence.from_dict(e) for e in artifact["evidence"]]) | ||
| and payload["evidence"] == (foundation_evidence(stage, result["payload"]) | ||
| if result["payload"].get("foundation_prompt_version") == 4 else | ||
| _Stage._judge_evidence(stage, [Evidence.from_dict(e) for e in artifact["evidence"]])) | ||
| and all(reply["dimensions"].get(d) == "yes" for d in | ||
| ("faithful", "does_not_weaken", "non_contradictory"))) | ||
| except (KeyError, ValueError, OSError, TypeError) as exc: | ||
|
|
@@ -323,6 +353,9 @@ def restore_jobs(stage): | |
| raise InitError("foundation checkpoint input identity mismatch") | ||
| for result in sorted(saved.get("tasks", {}).values(), key=lambda item: item["sequence"]): | ||
| _apply(stage, result) | ||
| # This run has already replayed these immutable results, including native | ||
| # approvals. Feedback preparation need not replay their archives twice. | ||
| stage._foundation_validated_results = {r["result_sha256"] for r in saved.get("tasks", {}).values()} | ||
|
|
||
|
|
||
| def _validate_fresh(stage, result): | ||
|
|
@@ -346,6 +379,64 @@ def _validate_fresh(stage, result): | |
| result["result_sha256"] = _hash({k: v for k, v in result.items() if k != "result_sha256"}) | ||
|
|
||
|
|
||
| def _with_review_feedback(stage, jobs, saved): | ||
| """Only a same-batch v4 resume gets advisory, per-facet correction history.""" | ||
| for original in jobs: | ||
| if (not stage.rt.unlimited_subscription | ||
| or original["payload"].get("foundation_prompt_version") != 4): | ||
| yield original | ||
| continue | ||
| if saved.get("tasks") and saved.get("binding") != stage.record.inputs_digest: | ||
| raise InitError("foundation feedback needs the same frozen batch identity") | ||
| history = {} | ||
| for prior in saved.get("tasks", {}).values(): | ||
| if (prior.get("owner") != original["owner"].owner or prior.get("page") != original["page"] | ||
| or prior.get("payload", {}).get("owner") != original["owner"].owner | ||
| or prior.get("payload", {}).get("foundation_prompt_version") != 4 | ||
| or not set(prior.get("requested", [])) & set(original["requested"])): | ||
| continue | ||
| if prior.get("result_sha256") != _hash({k: v for k, v in prior.items() if k != "result_sha256"}): | ||
| raise InitError("foundation feedback prior result hash mismatch") | ||
| if prior.get("result_sha256") not in getattr(stage, "_foundation_validated_results", set()): | ||
| _validate_result(stage, prior) | ||
| history.setdefault(prior["input_sha256"], prior) | ||
| feedback, requested = {}, [] | ||
| for facet in original["requested"]: | ||
| attempts = sorted((p for p in history.values() if facet in p["requested"]), | ||
| key=lambda p: p["sequence"]) | ||
| if len(attempts) >= 4: # Initial extraction plus at most three corrections. | ||
| stage.record.unfinished.append(f"{original['owner'].owner}/{facet}: three review corrections exhausted; remains unknown") | ||
| continue | ||
| requested.append(facet) | ||
| if attempts: | ||
| latest = attempts[-1] | ||
| key = f"knowledge:{original['owner'].owner}:{facet}" | ||
| verdict = latest.get("verdicts", {}).get(key, {}) | ||
|
Comment on lines
+411
to
+414
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an initial draft receives useful rejection reasons but its first correction is checkpointed with a generator/model error, Useful? React with 👍 / 👎. |
||
| if (verdict.get("page") != original["page"] or verdict.get("facet") != facet | ||
| or verdict.get("verdict") not in {"fail", "unsure", "unjudged"}): | ||
| verdict = {} # Unavailable advisory review, never another facet's reasons or an approval. | ||
| feedback[facet] = {"correction_round": len(attempts), | ||
| "prior_attempts": [{"input_sha256": p["input_sha256"], "result_sha256": p["result_sha256"]} | ||
| for p in attempts], | ||
| "last_verdict": verdict.get("verdict", "unknown"), | ||
| "last_review_reasons": copy.deepcopy(verdict.get("reasons", {})), | ||
| "last_error": latest.get("error", ""), | ||
| "last_dropped_reasons": [p["why"] for p in latest.get("dropped", []) | ||
| if p.get("rule_id") == key and p.get("page") == original["page"]]} | ||
| if not requested: | ||
| continue | ||
| if not feedback and requested == original["requested"]: | ||
| yield original | ||
| continue | ||
| job = {**original, "payload": copy.deepcopy(original["payload"]), "requested": requested} | ||
| job["payload"]["facets"] = requested | ||
| if feedback: | ||
| job["payload"]["prior_review_feedback"] = {"advisory_only": True, | ||
| "scope": "Previous rejection/error data for these requested facets; not source evidence or approval.", | ||
| "facets": feedback} | ||
| yield job | ||
|
|
||
|
|
||
| def run_jobs(stage, jobs): | ||
| """One worker's generator and judges are serial, so the pool is the shared cap.""" | ||
| workers = parallelism(stage.rt) | ||
|
|
@@ -358,6 +449,9 @@ def run_jobs(stage, jobs): | |
| pacer.prepare() | ||
| stage.rt.gateway.configure_zcode_pacing(pacer) | ||
| saved = stage.record.coverage.setdefault("foundation_jobs", {"binding": stage.record.inputs_digest, "tasks": {}}) | ||
| jobs = list(_with_review_feedback(stage, jobs, saved)) | ||
| if not jobs: | ||
| return | ||
| completed = {} | ||
| sequence = max((r["sequence"] for r in saved["tasks"].values()), default=-1) + 1 | ||
| with ThreadPoolExecutor(max_workers=workers, thread_name_prefix="kb-foundation") as pool: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A checkpoint containing only a rejected facet has no artifact—and therefore no archived judge receipt—for
_validate_resultto authenticate. Anyone able to alter that checkpoint can replaceverdicts[*].reasons, recompute the ordinaryresult_sha256, and have these lines accept the fabricated reasons as retry feedback; the next native generator trace then permanently binds the tampered prompt and passes subsequent replay. Store and verify a native rejection receipt, rather than treating a recomputable result hash as validation, before supplying rejection data to a correction.Useful? React with 👍 / 👎.