"""Render synthetic wire/protocol examples for each catalogue journey.

Ids, revisions, grants, observations and responses are illustrative fixtures.
No model invocation, HTTP request or target operation is performed.
"""
from pathlib import Path
import json
import hashlib
from case_ui_model import derive, all_steps, bind_protocols, verification_profile
from agent_catalog import by_profile as catalogue_profile

HERE = Path(__file__).resolve().parent
ARCH = HERE.parent

KARPATHY = """Working discipline (published protocol, adapted from CLAUDE_andrej-karpathy.md):
1. State assumptions. Separate confirmed evidence, hypothesis and missing information.
2. Make the smallest change that achieves this task; add no optional feature or unrelated cleanup.
3. Preserve unrelated state and work. Work only at the authorised target and scope.
4. Define success as explicit checks. Reconcile the whole plan; missing evidence is not success.
Questions and dead ends go to the parent. Only the root presents unresolved questions to a human.
These rules guide reasoning. Runtime grants and capability checks enforce authority."""


def digest(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def wire(method, path, body=None, response="200; current authorised projection"):
    value = dict(method=method, path=path, response=response)
    if method not in ('GET','HEAD'):
        wire.sequence += 1
        value['headers'] = {'Idempotency-Key':f'fixture-command-{wire.sequence:05d}'}
    if body is not None:
        value["body"] = body
    return value


def role_task(profile, case):
    if profile == "commercial.assessor":
        return "Clarify the actual request and assess its required work in the same task. Use the communication method for questions through root. Consult domain owners read-only; distinguish active effort from waiting and record estimate ranges/uncertainty. Do not implement. " + case["investigate"]
    if profile == "commercial.offer_preparer":
        return "After clarification and assessment, prepare the offer from the complete assessed scope and owner-provided terms. Preserve evidence, assumptions and missing commercial decisions; return the packet for review before customer acceptance. " + case["verify"]
    if profile == "commercial.coordinator":
        return "Coordinate assessment and offer preparation, review exact terms, obtain the customer decision and coordinate final communication. Acceptance grants no implementation authority. " + case["verify"]
    if profile == "support.solution_validator":
        return "Review the packet independently for its established mechanism, complete target scope, exact steps, expected counts, assertions and recovery. Return review evidence to the root for H3; never approve or execute your own packet. " + case["falsifier"]
    if profile == "support.resolution":
        return "Check compatible active experience, skills and tools against fresh target/version/lifecycle evidence. Record matches and rejected discriminators. Missing or contradicted evidence requests S2; otherwise prepare the bounded solution. " + case["execute"]
    if profile == "support.application":
        return "Coordinate only the current approved steps through the execution service, or park on the assigned external-owner action. Report actual outcomes; reconcile unknown effects before retry. Do not verify your own result. " + case["execute"]
    if profile == "support.recovery":
        return "Compare the original effect journal and pre-image with current state. Propose bounded compensation or required restoration, preserve intervening external edits, and return its result for independent S6 validation. " + case["falsifier"]
    if profile.endswith(".triage"):
        return "Answer the root's specific ambiguous classification question using current policy and compatible precedents. Optional adviser only; do not replace root classification or routing. " + case["investigate"]
    if profile.endswith(".root"):
        return "Own the requested outcome. Select declared stages and specialists, confirm child plans, join their assertions and retain external/restoration obligations. " + case["verify"]
    if "validator" in profile or "reviewer" in profile:
        return "Independently test the proposed result against current evidence. Return agree/disagree with separating evidence. Do not read the investigator's transcript. " + case["verify"]
    if profile.endswith("write_auditor"):
        return "Review exactness, boundedness, reversibility, authority scope and mechanism for the current packet. Return pass/refuse with failed items. Never execute it. " + case["execute"]
    if profile.endswith("communicator"):
        return "Draft the actual achieved result and outstanding work in customer language. No database details in the client view. Sending requires its own granted operation. " + case["verify"]
    if profile.endswith("curator"):
        return "Record actual outcome and independent uses. File proposed experience and hand domain changes to the canonical owner; do not auto-publish or grant authority. " + case["learn"]
    return ("Establish: " + case["investigate"] + "\nPrepare the bounded work: " + case["execute"]
            + "\nReturn evidence sufficient to test: " + case["verify"]
            + "\nReject this false completion: " + case["falsifier"])


def build():
    wire.sequence = 0
    cases = json.loads((HERE/"case-journeys.json").read_text(encoding="utf-8"))["cases"]
    journeys=derive(cases)
    profile_nodes = {}
    for p in (ARCH/"contracts/manifests").glob("*.json"):
        for profile in json.loads(p.read_text())["profiles"]:
            if profile["owns"]:
                profile_nodes[profile["key"]] = profile["owns"][0]
    for profile in ('support.solution_validator','support.application','support.result_validator','support.recovery'):
        profile_nodes[profile]='support/methodologies'
    result = {}
    for original in cases:
        c={**original,'profiles':list(dict.fromkeys(original['profiles']+[s['profile'] for s in all_steps(journeys[original['id']]) if s['profile']!='source.test_runner']))}
        cid = "fixture-" + c["id"].lower()
        plan = "plan-" + cid
        working_module=journeys[c['id']]['ui']['working_module']
        root = "support.root" if working_module in ('data','commercial') else working_module+'.root'
        validator = 'commercial.coordinator' if working_module=='commercial' else verification_profile(working_module,'joint')
        domain_validators = [verification_profile(s['module'],s['stage']) for s in all_steps(journeys[c['id']]) if s['module']=='configuration' and s['stage'] in ('S2','S3','S4','S5','joint','release')]
        work_roots = [p.split('.')[0]+'.root' for p in c['profiles'] if p.startswith(('source.','configuration.'))]
        profiles = list(dict.fromkeys(["support.root",root] + work_roots + c["profiles"] + domain_validators + [validator,"support.communicator","support.curator"]))
        inits = []
        for i, profile in enumerate(profiles):
            text = role_task(profile, c)
            definition = catalogue_profile(profile)
            text += '\n[PROFILE RESPONSIBILITY]\n'+definition['purpose']+'\n'+'\n'.join(definition.get('instructions',[]))
            if definition.get('context_contract'):text+='\n[CONTEXT CONTINUITY]\n'+definition['context_contract']
            node = profile_nodes.get(profile, profile.split(".")[0])
            if profile.startswith('configuration.') and profile.endswith('.validator'):
                domain=profile.split('.')[1];node='configuration' if domain=='integration' else 'configuration/'+domain
            prompt = ("[TRUSTED PLATFORM/PROFILE PROTOCOL]\n" + KARPATHY
                      + "\nRole: " + profile + ". Return model-turn v1 only.\n"
                      + "[EXECUTABLE KNOWLEDGE]\nRetrieve published skill/tool usage contracts before repeating mechanical work. Tools are pinned source code and/or binaries. Run a bound tool segment without an LLM call per item; retain interpretation at explicit judgement points. If repeated reasoning, latency or inconsistent output reveals missing automation, propose a bounded tool, tests and its skill/index binding. Do not load binary bytes into this prompt.\n"
                      + "[REGISTERED TOOLS AND AUTHORITY]\nInitial estate access is read-only: connectors.read and permitted read/computation tool bindings. Owned case papers may be read/written; memory.read/search retrieves evidence. "
                      + "Return a Plan or Question when more work is needed. No target write, send, deployment or grant creation tool is exposed.\n"
                      + "[PARENT TASK, CURRENT PLAN]\n" + text + "\nCase: " + cid + "; plan revision: 1.\n"
                      + "[DOMAIN KNOWLEDGE, DERIVED EVIDENCE]\nRead the pinned " + node + "/AGENTS.md index. "
                      + "Historical catalogue evidence is a candidate: " + c["source"] + ". Re-check its current applicability. "
                      + "Initial knowledge context: " + c["investigate"] + "\n"
                      + "[CURRENT CASE PAPERS]\nOriginal objective and source citations, approved scope/specification, current baselines, relevant evidence and predecessor results, decisions, known uncertainties, dependencies and required assertions. "
                      + "Unavailable papers must be requested, not invented. Fixture paper references below stand for stored bytes.\n"
                      + "[OUTPUT]\nOne schema-valid tool_calls or message object. A stated summary and evidence references, not hidden chain-of-thought. "
                      + "Unproved or externally reported results remain unproved.")
            inits.append({"task_id":f"task-{cid}-{i}","parent_task_id":None if i==0 else f"task-{cid}-0",
                          "profile_key":profile,"profile_version":1,"prompt_version":1,"model_route":"fixture-readonly-route-v1",
                          "execution_kind":"llm","memory_node":node,"state":"open","lease_owner":None,"fence_epoch":0,"runtime_version":"fixture-v1","grant_subset":["fixture-read-grant"],"budget_minutes":10,
                          "pinned_inputs":{"manifest":"fixture-manifest-v1","case_type":"fixture-"+c["id"],"plan_revision":1},
                          "task_text":text,"permitted_preparation_tools":["papers.read","papers.write","memory.read","memory.search","connectors.read","tools.run"],
                          "denied_context":["investigator_transcript"] if "reviewer" in profile or "validator" in profile else [],
                          "rendered_input":prompt,"input_sha256":digest(prompt)})
        by_profile = {t['profile_key']:t for t in inits}
        for task in inits:
            profile = task['profile_key']
            parent_profile = profile.split('.')[0]+'.root'
            if profile in ('commercial.assessor','commercial.offer_preparer'):
                parent_profile='commercial.coordinator'
            if profile == 'support.root':
                task['parent_task_id'] = None
            elif profile.endswith('.root') or parent_profile not in by_profile:
                task['parent_task_id'] = by_profile['support.root']['task_id']
            else:
                task['parent_task_id'] = by_profile[parent_profile]['task_id']
            task['phase_tool_catalogues'] = {
                'preparation':task['permitted_preparation_tools'],
                'after_parent_plan_confirmation':['papers.write','tasks.consult','tasks.spawn','skills.run','tools.run','writes.propose'],
                'after_placement_approval':['worktree.edit','worktree.diff','worktree.build'] if profile.startswith('source.') else [],
                'post_result':['papers.write','memory.propose'],
                'restriction':'Each list is intersected with the published profile, current grants and capabilities. No model receives direct mutating connector or grant creation tools. gates.request is root-only.'}
        for task in inits:
            if any(word in task['profile_key'] for word in ('reviewer','validator','write_auditor')):
                task['phase_tool_catalogues']['after_parent_plan_confirmation']=['papers.write','skills.run.read','tools.run.read']
                task['phase_tool_catalogues']['after_placement_approval']=[]
        by_task = {t['task_id']:t for t in inits}
        def task_path(t):
            return 'root' if t['parent_task_id'] is None else task_path(by_task[t['parent_task_id']])+'/'+t['profile_key'].replace('.','-')
        for task in inits:
            task['task_path'] = task_path(task)
        if any(s['module']=='source' and s['stage']=='S4' for s in all_steps(journeys[c['id']])):
            inits.append({'task_id':'task-'+cid+'-tests','parent_task_id':by_profile['source.root']['task_id'],'profile_key':'source.test_runner','profile_version':1,'stage_id':'S4','state':'open','execution_kind':'deterministic','rendered_input':None,'task_text':'Run the declared build/test commands in the confined worktree and return their actual results.','dispatch':'Only after the source plan and task dependencies permit it.'})
        sender={"task_id":f"task-{cid}-0","profile_key":"support.root","profile_version":1,"runtime_version":"fixture-v1"}

        def message(kind,payload,paper=None,recipient=None):
            m={"type":kind,"version":1,"message_id":f"message-{cid}-{kind}","case_id":cid,"task_path":by_task[sender['task_id']]['task_path'],
               "sender":sender,"occurred_on":"2026-09-07T09:00:00Z","trust_class":"derived","payload":payload}
            if recipient:
                m["recipient_task_id"]=recipient
            if paper:
                m["artefact"]={"hash":digest(paper),"paper_ref":paper}
            return m

        # the spawned sub-agent is the case's first working profile: never a root, and not the shared communicator or curator when a working profile exists
        work=[p for p in c["profiles"] if not p.endswith('.root') and p not in ('support.communicator','support.curator')] or [p for p in c["profiles"] if not p.endswith('.root')]
        specific=next(t for t in inits if t["profile_key"]==work[0])
        parent=next(t for t in inits if t.get('task_id')==specific['parent_task_id'])
        sender={**sender,'task_id':parent['task_id'],'profile_key':parent['profile_key']}
        spawn=message("Spawn",{"kind":"sub_agent","profile_key":specific["profile_key"],"profile_version":1,
            "task_text":specific["task_text"],"budget_minutes":10,"grant_subset":["fixture-read-grant"],"memory_node":specific["memory_node"],"denied_context":[]},recipient=specific["task_id"])
        proposed=message("Plan",{"plan_id":plan,"revision_no":1,"step_count":2,"open_questions":0},"paper:two-step-illustration")
        proposed["sender"]={**sender,"task_id":specific["task_id"],"profile_key":specific["profile_key"]}
        proposed["task_path"]=specific['task_path']; proposed['recipient_task_id']=parent['task_id']
        confirmation=message("PlanConfirmation",{"plan_id":plan,"revision_no":1,"decision":"confirmed","scope_confirmed":["establish","propose"]},recipient=specific["task_id"])
        expected=message("Result",{"proof_level":"structural","reconciliation":{"steps_planned":2,"steps_done":2,"assertions_passed":1,"assertions_failed":0},"missing_open":0},"paper:prepared-plan-not-target-proof")
        expected["sender"]=proposed["sender"]; expected["task_path"]=specific['task_path']; expected['recipient_task_id']=parent['task_id']
        refusal=message("Verdict",{"verdict":"refuse","checks":[{"check":"mechanism","result":"refuse","failing_item":c["falsifier"],"evidence_ref":"paper:falsifying-evidence"}]},"paper:refusal")
        refusal["sender"]={**sender,"task_id":by_profile[validator]['task_id'],"profile_key":validator}
        refusal['task_path']=by_profile[validator]['task_path']; refusal['recipient_task_id']=parent['task_id']
        tool_response={"kind":"tool_calls","summary":"Read current scope before choosing a historical solution.","tool_calls":[{"name":"connectors.read","arguments_json":json.dumps({"operation_id":"fixture.scope.read","input":{"case_scope_ref":"paper:current-scope"}})}],"message":None}
        model_plan={"kind":"message","summary":"Return the bounded preparation plan to the parent.","tool_calls":[],"message":{"type":"Plan","payload_json":json.dumps(proposed["payload"]),"paper_ref":"paper:two-step-illustration"}}
        result[c["id"]]={"status":"Synthetic protocol examples; fixture versions/ids/evidence are not published or observed results. The two-step Plan/Result illustrates preparation only. Budget 10 is fixture data, not an operating default.",
          "api_base":"/ai-support/api/v1","agent_initializations":inits,"model_responses":[tool_response,model_plan],
          "messages":[spawn,proposed,confirmation,expected,refusal],
          "connector_execution":{"input_type":"WritePacketCall", "fields":["Call: OperationId, Version, Scope, InputJson, IdempotencyKey, CaseId, TaskId, RuntimeCallId, FenceEpoch","ShapeKey/ShapeVersion, ArtefactHash, BoundStep[] with template hashes, concrete parameters and expected counts","Assertion[] with query/call and expected result, teardown hashes, PreflightRequired"],"coordinator":"Resolve exact immutable packet and current GrantView; compare hash/scope, all decision ids, expiry, ReservedCallId and FenceEpoch immediately before dispatch.","forms":{"A":"One deterministic connector operation: apply uncommitted, verify local assertions and consuming query, then commit; failure rolls back locally.","B":"Leased deterministic multi-call transaction; no model/provider or human wait while open; expiry rolls back.","E":"External/DDL effect: dispatch once, journal observed result, reconcile unknown/partial outcome with WhatLandedAsync before retry."},"output_type":"WriteResult: StepOutcome[] expected/actual counts and returned ids; VerifyResult assertions/level; actual statement/call; teardown reference; nullable commit time; LandedDisposition.","case_specific_execution":c['execute'],"case_specific_proof":c['verify'],"reversal":"Before a committed effect, record its exact inverse/compensation. After commit, an H7 packet uses the original journal plus current target state; its execution has a new operation identity. Uncommitted local failure rolls back immediately. Irreversible residues stay explicit."},
          "provider_call":{"interface":"IInferenceClient.ExecuteAsync(InferenceRequestV1)","input":"Exact rendered_input bytes above; one input document in the copied adapter.","output_schema":"contracts/model-turn.schema.json","persistence":"PAPERS request bytes and hash before dispatch; RUNTIME_CALLS request/ref/disposition; response bytes then action validation before tools."},
          "operations":{
           "intake":{"http":[wire("POST","/client/requests",{"module":"support","description":c["title"],"attachments":[]},"201; case/arrival identities; authority gaps return 202"),wire("GET","/cases/{caseId}"),wire("GET","/client/requests/{caseId}")],"storage":["ARRIVALS: canonical origin, received time, source/body hashes, state and case id","CASES: objective, customer, selected case type, clocks and opened state","PAPERS: normalised request and initial context; CASE_HANDLES: protected identity map","INBOX_MESSAGES + OUTBOX_MESSAGES + AUDIT_LEDGER: command dedup, queued root classification and event, committed atomically"],"initialization":"Create or resume one Support root task for classification and routing. An optional classification adviser is requested only for a specific ambiguity; other child tasks open when the route/plan permits them; the examples list the eventual roles, not an eager spawn of every role."},
           "evidence":{"http":[wire("GET","/cases/{caseId}/cards"),wire("GET","/papers/{caseId}"),wire("GET","/cases/{caseId}/where")],"storage":["TASKS: pinned profile/version, task text, parent, stage, restricted grants, memory node, lease/fence","PAPERS: current inventory, evidence, exact model input/output; RUNTIME_CALLS: model intent/result","TURN_CHECKPOINTS: state-paper id/hash, pending calls/messages and next turn; MEMORY_USES: actual retrieval/use verdict"],"initialization":"Run the selected specialist preparation prompt below. Reads go through a registered scoped operation; fixture.scope.read is a test adapter placeholder, not an estate operation that exists."},
           "decision":{"http":[wire("GET","/decisions/gates/{gateId}"),wire("POST","/decisions/gates/{gateId}/decide",{"decision":"approve","artefact_hash":digest("fixture-packet"),"expected_version":"4","reason":"Fixture: approve the displayed scope."},"200 decision; 409 stale version/hash; 403 wrong role")],"storage":["PLANS + immutable PLAN_REVISIONS: confirmed scope/hash; PLAN_STEPS: stable ids, target, effects and assertion references","PAPERS: semantic packet, execution-obligation matrix, audit/validator reports","GATES + GATE_DECISIONS: checkpoint/effect decisions; GRANTS: only eligible exact effect scope; no write on plan confirmation"],"initialization":"Specialist Plan returns to its immediate parent; PlanConfirmation orders work. The root raises the applicable gate; parent confirmation does not replace it."},
           "execution":{"http":[wire("POST","/client/requests/{caseId}/responses",{"request_ref":"paper:external-instruction","expected_version":"5","response_kind":"report_completed","step_id":"external-step","plan_revision":1,"text":"Fixture: I performed the requested action; please verify.","evidence_refs":["paper:uploaded-evidence"]},"202 checking; only when an assigned external step exists"),wire("GET","/cases/{caseId}/cards")],"storage":["External branch: response paper + TASKS parked/reopened; INBOX_MESSAGES/OUTBOX_MESSAGES atomically schedule verification","Platform branch: RUNTIME_CALLS intent; IMPACT_LOCKS and grant reservation; connector result; WRITE_LOG only for platform effects","BUILD_STATE records verified completed steps/returned ids; TURN_CHECKPOINTS records next work; no model holds a transaction"],"initialization":"The response POST is conditional on external work. Automatic execution needs no extra browser Execute endpoint: the execution coordinator dispatches after the gate. It constructs OperationCall from the approved packet and current grant/fence."},
           "verification":{"http":[wire("GET","/cases/{caseId}/where"),wire("GET","/papers/{caseId}/{kind}/{name}")],"storage":["PAPERS: per-member assertion results with exact query/tool and evidence revision","Execution paper: which required checks passed/failed; PLAN_STEPS and BUILD_STATE update only on verified evidence","RUNTIME_CALLS/WRITE_LOG remain authoritative for what landed; no successful assertion is invented from model text"],"initialization":"Initialise an independent validator with current evidence and required assertions; deny the investigator transcript. Required proof: "+c["verify"]},
           "reversal":{"http":[wire("GET","/decisions/gates/{gateId}"),wire("POST","/decisions/gates/{gateId}/decide",{"decision":"approve","artefact_hash":digest("fixture-reversal-packet"),"expected_version":"6","reason":"Fixture: approve the displayed H7 compensation."},"200 H7 decision; separate effect prerequisites still compose")],"storage":["PAPERS: reversal packet derived from original effect journal, before image and current comparison","GATES/GATE_DECISIONS/GRANTS: H7 plus applicable target/effect permissions","RUNTIME_CALLS: new reversal operation id, original call retained; WRITE_LOG: inverse/compensating effect and proof; suspended shape recorded when required"],"initialization":"Failure returns to the responsible root. Re-read current target state, propose the exact reversal, and run only the authorised compensation. For a DB-local uncommitted validation failure the deterministic connector rolls back immediately; no H7 is needed to avoid committing. After commit, never overwrite later external changes. A read-only answer/referral has no target rollback; correct its record and communicate through the send gate."},
           "delivery":{"http":[wire("POST","/client/requests/{caseId}/accept",{"gate_id":"fixture-delivery-gate","artefact_hash":digest("fixture-delivered-result"),"decision":"accept","scope":"delivery","expected_version":"7","note":"Fixture: the stated result is accepted."},"200 only for the shown customer-owned acceptance; early proof gaps remain visible")],"storage":["CASES: resolved only after obligation join; GATE_DECISIONS: the corresponding acceptance","PAPERS: achieved outcome and remaining/successor obligations; channel-send call/evidence retained","OUTBOX_MESSAGES post-result job and resolution commit together; customer confirmation closes after required technical proof"],"initialization":"Communicator receives a verified outcome summary, not unrestricted source transcripts. Preparation of scripts, preview and a completed subcase do not silently change the parent's objective."},
           "retention":{"http":[wire("GET","/knowledge/proposals"),wire("GET","/papers/{caseId}")],"storage":["PAPERS: sanitised experience and precipitation decision linked to result revision","EXPERIENCE_ENTRIES: proposed index entry with actual outcome and execution-record reference","MEMORY_ARTICLES: proposed owned-domain change; AUDIT_LEDGER: justified nothing, reuse or correction; OUTBOX/INBOX prevents duplicate post-result work"],"initialization":"Initialise support.curator and, where needed, the actual domain owner with the retained sanitised result. "+c["learn"]},
           "publication":{"http":[wire("POST","/studio/{subject}/{key}/{version}/evaluate",None,"202 evaluation queued"),wire("POST","/knowledge/articles/{articleId}/confirm",None,"200 active only for the authorised owner/review path"),wire("POST","/studio/{subject}/{key}/{version}/publish",None,"200 published; 403/409/422 when authority, revision or publish prerequisites fail")],"storage":["EVAL_SETS/evaluation records: pinned dependency versions and positive/negative case results","MEMORY_USES: independent canonical requests and verification verdicts; MEMORY_ARTICLES/registry new version on publication","WRITE_SHAPES/RISK_ACCEPTANCES are separate authority records and are never changed merely by publishing knowledge"],"initialization":"Run the affected profile eval set with held-out request/causal families. Corrections are new immutable versions; source/proposed material never becomes authority."},
           "recurrence":{"http":[wire("POST","/client/requests",{"module":"support","description":"A new independent request of the same kind: "+c["title"],"attachments":[]},"201 new case; delivery retry with the original key remains the original request"),wire("GET","/knowledge/experience/search",response="200 compatible active candidates; this is the operator API, not a customer data feed")],"storage":["New CASES/ARRIVALS/CASE_HANDLES and fresh plan/grants; origin aliases dedup only the same source request","MEMORY_USES records candidate selection, pinned article/skill version and current applicability proof","PAPERS: fresh preflight and discriminating evidence; no old result/grant reused as current proof"],"initialization":"A new root classifies and routes using current published revisions; selected specialists receive the relevant whole-case brief. Old task context and authority are not copied. "+c["next"]}
          }}
    for original in cases:
        entry=result[original['id']]
        fragments=entry.pop('operations')
        entry['journey']=journeys[original['id']]
        entry['stage_traces']=bind_protocols(original,entry['journey'],fragments,entry)
    (HERE/"case-runtime-traces.json").write_text(json.dumps(result,ensure_ascii=False,indent=2)+"\n",encoding="utf-8")
    return result


if __name__ == "__main__":
    print(f"Built synthetic protocol traces for {len(build())} cases")
