Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
51 KiB
JSON
1 line
51 KiB
JSON
{"repo": "bcy2020/agent-chronos-arch", "files": {"experiment/decomposer-mental-model-study/test_multistage_exp03_pipeline_regression.py": {"4b1d1082df5e225f143a1d4fe8c3f5c2ff9b49cf": {"t": 1786068358.755393, "n": 49216, "h": "7942842f653e9996", "c": "\"\"\"\nExperiment 3: Shallow Pipeline Regression\n\nCompares three decomposition conditions on real pipeline errors:\n - single_stage_baseline: current mvp-0.4.4 decomposer prompt\n - single_stage_notraditional: + no-traditional-pattern principle\n - three_stage: Stage 1 + Stage 2 + Stage 3 merged\n\nOnly root-level decomposition/composition, not full recursive tree building.\n\nOutput: output/multistage_exp03_pipeline_regression/{model}/\n\"\"\"\nimport json, os, sys, time, re, argparse\nfrom collections import defaultdict\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom dotenv import load_dotenv\nload_dotenv(os.path.join(os.path.dirname(__file__), \".env\"))\n\nfrom openai import OpenAI\n\ndef _env(key, default=\"\"):\n return os.getenv(key) or os.getenv(f\"DEEPSEEK_{key.removeprefix('CHRONOS_')}\") or default\n\nTEMPERATURE = float(os.getenv(\"CHRONOS_TEMPERATURE\", \"0.3\"))\nMAX_TOKENS = int(os.getenv(\"CHRONOS_MAX_TOKENS\", \"16384\"))\nMAX_CONCURRENCY = 5\n\nOUTPUT_DIR = os.path.join(os.path.dirname(__file__), \"output\", \"multistage_exp03_pipeline_regression_conservation_prompt\")\n\n# Import pipeline models and decomposer\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"..\", \"mvp\", \"mvp-0.4.4\"))\nfrom models import (\n Node, InputParam, OutputParam, Boundary, GlobalVar, DataSource,\n SubPRD, AcceptanceCriterion, ChildContract, DataOperation,\n)\nfrom decomposer import Decomposer\nfrom config import Config as PipelineConfig\n\n# Import decomposer_cases for case definitions\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"test_data\"))\nfrom decomposer_cases import get_cases, ALL_CASES\n\n# ========================================================================\n# Condition prompts\n# ========================================================================\n\nNOTRADITIONAL_ADDITION = \"\"\"\n\nDO NOT ASSUME TRADITIONAL DEVELOPMENT PATTERNS:\n- DO NOT create a \"dispatcher\", \"router\", \"controller\", \"command_handler\", or similar node that delegates work to other children.\n- DO NOT use the Command Pattern, Strategy Pattern, or any design pattern where one child calls other children.\n- DO NOT create a \"coordinator\" node whose primary purpose is to figure out which other child to call.\n- Each child MUST be a self-contained function that does actual work.\n- The parent IS the router. If different inputs need different processing, the parent decides through conditional logic which child to call.\n- The purpose of decomposition is to divide work, not to recreate enterprise architecture patterns.\"\"\"\n\n\nSTAGE1_SYSTEM_PROMPT = \"\"\"You are a software system decomposition agent. Your task is to decompose a function block into smaller child function blocks.\n\nCRITICAL RULES:\n1. EVERY CHILD MUST BE A FUNCTION, NOT A CLASS.\n2. TREE STRUCTURE (not graph): Children MUST NOT call each other. The parent MUST directly invoke all children.\n3. Do NOT add extra external inputs or outputs beyond what the parent has.\n4. Children should be at the same abstraction level and minimally overlapping.\n\nDO NOT ASSUME TRADITIONAL DEVELOPMENT PATTERNS:\n- DO NOT create \"dispatcher\", \"router\", \"controller\", \"command_handler\" nodes.\n- Each child MUST be a self-contained function that does actual work.\n- The parent IS the router.\n\nSEMANTIC STOP CONDITIONS: STOP when pure function, atomic operation, or max depth reached.\n\nOUTPUT FORMAT — Return valid JSON:\n{\n \"children\": [\n {\n \"name\": \"ChildName\",\n \"purpose\": \"semantic responsibility\",\n \"behavior\": \"internal transformation without sibling calls\",\n \"boundary\": {\"in_scope\": [\"...\"], \"out_of_scope\": [\"...\"]},\n \"semantic_inputs\": [{\"name\": \"...\", \"description\": \"...\", \"source\": \"parent input | previous child output | constant | internal leaf access\"}],\n \"semantic_outputs\": [{\"name\": \"...\", \"description\": \"...\", \"consumer\": \"parent | ChildName\"}],\n \"preconditions\": [\"...\"],\n \"postconditions\": [\"...\"],\n \"guarantees\": [\"...\"],\n \"composition_role\": \"transform | validate | decide | execute | aggregate | query | mutate\",\n \"stop_decompose\": false,\n \"stop_reason\": \"\"\n }\n ],\n \"decomposition_rationale\": \"how children jointly cover parent responsibility\",\n \"orchestration_model\": \"sequence | conditional | aggregate | pipeline | mixed\",\n \"dataflow_sketch\": [{\"from\": \"parent | ChildName\", \"to\": \"ChildName | parent\", \"data\": \"semantic data\", \"note\": \"why\"}]\n}\n\nCONSTRAINTS:\n1. Child must not call, invoke, dispatch to, route to, or reference siblings.\n2. Parent is the only router/orchestrator.\n3. Do not use handler/router/dispatcher/controller patterns.\n4. Do NOT emit inputs, outputs, signature, global_vars, data_operations, requested_capabilities.\"\"\"\n\n\nSTAGE2_SYSTEM_PROMPT = \"\"\"You are an interface derivation agent. Given a frozen Stage 1 decomposition, derive precise typed interfaces for each child.\n\nRULES:\n1. Do NOT add, delete, rename, or reorder children.\n2. Do NOT change any Stage 1 field (purpose, behavior, boundary, preconditions, postconditions, guarantees, composition_role).\n3. Derive ONLY: inputs, outputs, signature for each child.\n4. Use precise Python types.\n5. Every child input must have an explicit source.\n\nOUTPUT FORMAT — Return valid JSON:\n{\n \"children\": [\n {\n \"name\": \"ChildName (UNCHANGED)\",\n \"purpose\": \"(UNCHANGED)\",\n \"behavior\": \"(UNCHANGED)\",\n \"boundary\": {\"in_scope\": [\"(UNCHANGED)\"], \"out_of_scope\": [\"(UNCHANGED)\"]},\n \"preconditions\": [\"(UNCHANGED)\"],\n \"postconditions\": [\"(UNCHANGED)\"],\n \"guarantees\": [\"(UNCHANGED)\"],\n \"composition_role\": \"(UNCHANGED)\",\n \"stop_decompose\": false,\n \"stop_reason\": \"\",\n \"inputs\": [{\"name\": \"param\", \"type\": \"str\", \"description\": \"desc\", \"source\": \"where data comes from\"}],\n \"outputs\": [{\"name\": \"result\", \"type\": \"dict\", \"description\": \"desc\", \"consumer\": \"who uses this\"}],\n \"signature\": \"def ChildName(param1: type1) -> return_type\"\n }\n ],\n \"interface_preservation\": {\"parent_inputs_covered_by\": {}, \"parent_outputs_produced_by\": {}},\n \"dataflow_edges\": [{\"from_node\": \"parent|ChildName\", \"from_output\": \"\", \"to_node\": \"ChildName|parent\", \"to_input\": \"\", \"note\": \"\"}]\n}\"\"\"\n\n\nSTAGE3_SYSTEM_PROMPT = \"\"\"You are a governance and resource derivation agent. Given frozen Stage 1 + Stage 2, derive resource allocation and governance fields.\n\nRULES:\n1. Do NOT change Stage 1 semantics or Stage 2 signatures.\n2. Do NOT add, delete, rename, or reorder children.\n3. Derive ONLY: global_vars, data_operations, requested_capabilities, constraints, acceptance_criteria, traceability, node_type.\n4. Each child's global_vars MUST be a subset of the parent's global_vars.\n\nGLOBAL STATE CONSERVATION — HARD REQUIREMENT:\nThe parent's global_vars are an architectural contract. You MUST distribute them to children.\nFor every parent global var, the union of all child global_vars MUST cover the parent's required operation.\nIf parent requires read_write on X, children must collectively cover both read and write on X.\nIt is valid to assign read and write to different children, but neither side may disappear.\nDo not only infer local child needs; first satisfy the parent conservation ledger, then assign operations to responsible children.\nA child global_vars variable must come from parent global_vars — do not invent new variables.\ndata_operations should be consistent with global_vars.\nrequested_capabilities should reflect the same resource operation needs.\nDo not silently drop any parent operation.\n\nSELF-CHECK before returning JSON:\n- List every parent global var and its required op.\n- For each, confirm which child (or children) covers it.\n- If any parent op is unassigned, fix it before responding.\n\nOUTPUT FORMAT — Return valid JSON:\n{\n \"children\": [\n {\n \"name\": \"ChildName (UNCHANGED)\",\n \"purpose\": \"(UNCHANGED from Stage 1)\",\n \"behavior\": \"(UNCHANGED from Stage 1)\",\n \"inputs\": [\"(UNCHANGED from Stage 2)\"],\n \"outputs\": [\"(UNCHANGED from Stage 2)\"],\n \"signature\": \"(UNCHANGED from Stage 2)\",\n \"global_vars\": [{\"variable\": \"var_name\", \"op\": \"read|write|read_write\", \"description\": \"\"}],\n \"data_operations\": [{\"source_name\": \"source\", \"operation_type\": \"read|write|read_write\", \"description\": \"\"}],\n \"requested_capabilities\": [\"resource.operation\"],\n \"constraints\": [{\"constraint_id\": \"C-001\", \"description\": \"\"}],\n \"acceptance_criteria\": [{\"ac_id\": \"AC-001\", \"description\": \"\"}],\n \"traceability\": {\"parent_requirement_ids\": [\"FR-001\"]},\n \"node_type\": \"pure_function|atomic_operation\",\n \"stop_decompose\": false,\n \"stop_reason\": \"\"\n }\n ],\n \"governance_notes\": \"\"\n}\"\"\"\n\n# ========================================================================\n# Shared utilities\n# ========================================================================\n\nclass LLMLogger:\n def __init__(self, log_dir, api_key, base_url, model):\n self.log_dir = log_dir; self.api_key = api_key; self.base_url = base_url; self.model = model\n self.call_counter = 0\n os.makedirs(log_dir, exist_ok=True)\n\n def chat(self, messages, max_tokens=None):\n self.call_counter += 1; call_id = self.call_counter\n max_tokens = max_tokens or MAX_TOKENS\n req = {\"call_id\": call_id, \"timestamp\": time.time(), \"messages\": messages, \"max_tokens\": max_tokens, \"model\": self.model}\n with open(os.path.join(self.log_dir, f\"{call_id:04d}_request.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(req, f, indent=2, ensure_ascii=False)\n client = OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=120)\n start = time.time()\n try:\n resp = client.chat.completions.create(**dict(model=self.model, messages=messages, temperature=TEMPERATURE, max_tokens=max_tokens, extra_body={\"thinking\": {\"type\": \"disabled\"}}))\n text = resp.choices[0].message.content\n except Exception as e:\n with open(os.path.join(self.log_dir, f\"{call_id:04d}_response.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump({\"call_id\": call_id, \"elapsed\": round(time.time()-start, 2), \"error\": str(e)}, f, indent=2)\n raise\n elapsed = time.time() - start\n with open(os.path.join(self.log_dir, f\"{call_id:04d}_response.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump({\"call_id\": call_id, \"elapsed\": round(elapsed, 2), \"response\": text}, f, indent=2)\n return text\n\n\ndef parse_json(text):\n text = text.strip()\n if text.startswith(\"```\"):\n text = re.sub(r\"^```[a-zA-Z0-9]*\\n?\", \"\", text)\n text = re.sub(r\"\\n?```$\", \"\", text)\n if \"}\" in text:\n text = text[:text.rfind(\"}\")+1]\n text = re.sub(r'(?<=[\\s:,\\[{])[fFrRuUbB]+(\")', r'\\1', text)\n try:\n return json.loads(text)\n except json.JSONDecodeError:\n m = re.search(r'\\{.*\\}', text, re.DOTALL)\n if m:\n try:\n return json.loads(m.group())\n except:\n pass\n return {\"error\": \"JSON parse failed\", \"raw\": text[:500]}\n\n\n# Routing detection\nROUTING_PATTERNS = [\n re.compile(r'calls?\\s+(?:the\\s+)?(?:appropriate\\s+)?(?:child\\s+)?(?:handler\\s+)?(\\w+)', re.IGNORECASE),\n re.compile(r'invoke[s]?\\s+(\\w+)', re.IGNORECASE),\n re.compile(r'dispatch(?:es)?\\s+(?:to\\s+)?(\\w+)', re.IGNORECASE),\n re.compile(r'route[s]?\\s+(?:to\\s+)?(\\w+)', re.IGNORECASE),\n]\nROUTER_NAME_PATTERNS = re.compile(r'(?:^router$|^dispatcher$|route|dispatch|parse.*input|parse.*command|process.*command)', re.IGNORECASE)\nROUTER_PURPOSE_PATTERNS = re.compile(r'(?:route[s]?\\s+(?:the\\s+)?(?:command|request|input)|dispatch(?:es)?\\s+(?:to\\s+)?(?:the\\s+)?(?:appropriate|correct|corresponding))', re.IGNORECASE)\n\n\ndef detect_routing(children):\n child_names = {c.get(\"name\", \"\") for c in children}\n sibling_calls = []\n for c in children:\n cname = c.get(\"name\", \"\")\n text = c.get(\"purpose\", \"\") + \" \" + c.get(\"behavior\", \"\")\n for p in ROUTING_PATTERNS:\n for m in p.finditer(text):\n target = m.group(1)\n if target in child_names and target != cname:\n sibling_calls.append({\"from\": cname, \"to\": target, \"method\": \"text_pattern\"})\n router_nodes = []\n for c in children:\n name = c.get(\"name\", \"\")\n combined = c.get(\"purpose\", \"\") + \" \" + c.get(\"behavior\", \"\")\n if bool(ROUTER_NAME_PATTERNS.search(name)) or bool(ROUTER_PURPOSE_PATTERNS.search(combined)):\n router_nodes.append(name)\n if router_nodes and len(children) > len(router_nodes):\n for router in router_nodes:\n for c in children:\n if c.get(\"name\", \"\") != router:\n sibling_calls.append({\"from\": router, \"to\": c.get(\"name\", \"\"), \"method\": \"structural_router\"})\n seen = set(); unique = []\n for sc in sibling_calls:\n k = (sc[\"from\"], sc[\"to\"], sc[\"method\"])\n if k not in seen: seen.add(k); unique.append(sc)\n return len(unique) > 0, unique\n\n\n# ========================================================================\n# Deterministic checks\n# ========================================================================\n\ndef check_child_count(children, expected_range=(2, 10)):\n n = len(children)\n return n < expected_range[0] or n > expected_range[1]\n\n\ndef check_missing_required_fields(children, condition):\n \"\"\"Check for required fields based on condition.\"\"\"\n missing = []\n if condition == \"three_stage\":\n # Full schema required\n required_per_child = [\"name\", \"purpose\", \"behavior\", \"inputs\", \"outputs\", \"signature\",\n \"global_vars\", \"data_operations\", \"constraints\", \"acceptance_criteria\",\n \"traceability\", \"node_type\"]\n else:\n # Single-stage: full pipeline schema\n required_per_child = [\"name\", \"purpose\", \"inputs\", \"outputs\", \"boundary\",\n \"preconditions\", \"postconditions\", \"behavior\", \"signature\",\n \"data_operations\", \"constraints\", \"acceptance_criteria\",\n \"global_vars\", \"traceability\", \"requested_capabilities\"]\n for c in children:\n cname = c.get(\"name\", \"?\")\n for f in required_per_child:\n if f not in c or c[f] is None:\n missing.append(f\"{cname}:{f}\")\n return missing\n\n\ndef check_dangling_inputs(children, parent_node):\n \"\"\"Check if child input sources can be traced.\"\"\"\n parent_input_names = {i.name for i in parent_node.inputs}\n earlier_outputs = set()\n dangling = []\n for c in children:\n cname = c.get(\"name\", \"\")\n for inp in c.get(\"inputs\", []):\n source = inp.get(\"source\", \"\")\n param_name = inp.get(\"name\", \"\")\n # Valid sources: parent input, earlier child output, constant, leaf capability\n if not source:\n dangling.append(f\"{cname}.{param_name}: no source declared\")\n continue\n source_lower = source.lower()\n is_valid = False\n # Check parent inputs\n for pname in parent_input_names:\n if pname.lower() in source_lower:\n is_valid = True; break\n # Check earlier child outputs\n if not is_valid:\n for prev_out in earlier_outputs:\n if prev_out.lower() in source_lower:\n is_valid = True; break\n # Check constants/config\n if not is_valid:\n if any(kw in source_lower for kw in [\"constant\", \"config\", \"default\", \"internal\", \"leaf\"]):\n is_valid = True\n if not is_valid:\n dangling.append(f\"{cname}.{param_name}: source '{source}' not traceable\")\n # Track this child's outputs for later children\n for out in c.get(\"outputs\", []):\n earlier_outputs.add(out.get(\"name\", \"\"))\n return dangling\n\n\ndef check_global_var_subset(children, parent_globals):\n \"\"\"Check if each child's global_vars is a subset of parent's.\"\"\"\n parent_vars = {(g.variable if hasattr(g, 'variable') else g.get(\"variable\", \"\")) for g in parent_globals}\n violations = []\n for c in children:\n cname = c.get(\"name\", \"\")\n for gv in c.get(\"global_vars\", []):\n var = gv.get(\"variable\", \"\") if isinstance(gv, dict) else gv.variable\n if var and var not in parent_vars:\n violations.append(f\"{cname}:{var}\")\n return violations\n\n\ndef check_global_var_union_gap(children, parent_globals):\n \"\"\"Check if parent's global var operations are covered by children's union.\"\"\"\n parent_ops = {}\n for g in parent_globals:\n var = g.variable if hasattr(g, 'variable') else g.get(\"variable\", \"\")\n op = g.op if hasattr(g, 'op') else g.get(\"op\", \"\")\n if var:\n parent_ops[var] = op\n\n child_ops = defaultdict(set)\n for c in children:\n for gv in c.get(\"global_vars\", []):\n var = gv.get(\"variable\", \"\") if isinstance(gv, dict) else gv.variable\n op = gv.get(\"op\", \"\") if isinstance(gv, dict) else gv.op\n if var:\n child_ops[var].add(op)\n\n gaps = []\n for var, needed_op in parent_ops.items():\n if var not in child_ops:\n gaps.append(f\"{var}: no child covers {needed_op}\")\n # For read_write, both read and write must be covered\n elif needed_op == \"read_write\":\n has_read = \"read\" in child_ops[var] or \"read_write\" in child_ops[var]\n has_write = \"write\" in child_ops[var] or \"read_write\" in child_ops[var]\n if not has_read:\n gaps.append(f\"{var}: read not covered\")\n if not has_write:\n gaps.append(f\"{var}: write not covered\")\n return gaps\n\n\n# ========================================================================\n# Composition check (simplified - uses LLM)\n# ========================================================================\n\nCOMPOSE_SYSTEM_PROMPT = \"\"\"You are a decomposition verifier. Check if a parent function CAN be correctly implemented by composing its child functions.\n\nTREE STRUCTURE RULES:\n1. Each child is independent. A child must NOT call, reference, or depend on any sibling.\n2. Sibling invisibility: children have no knowledge of each other.\n3. Parent is the ONLY node that directly calls its children.\n4. Data flow goes through parent — parent takes one child's output and passes it to another.\n\nIf any tree structure check fails, return cannot_compose with reason \"tree_structure_violation\".\n\nReturn ONLY valid JSON:\n{\n \"status\": \"ok | cannot_compose\",\n \"decomposition_feedback\": {\n \"reason\": \"tree_structure_violation | missing_child_input_source | cannot_satisfy_parent_output | other\",\n \"offending_child\": \"ChildName or empty\",\n \"violations\": [{\"from_node\": \"\", \"to_node\": \"\", \"rule\": \"\", \"details\": \"\"}],\n \"suggested_fix\": \"\",\n \"requires_redecomposition\": true\n }\n}\"\"\"\n\n\ndef check_cannot_compose(node, children_data, logger):\n \"\"\"Use LLM to check if decomposition can be composed.\"\"\"\n children_desc = []\n for c in children_data:\n sig = c.get(\"signature\", \"def {}({}) -> {}\".format(\n c.get(\"name\", \"\"),\n \", \".join(f\"{i.get('name', '')}: {i.get('type', 'Any')}\" for i in c.get(\"inputs\", [])),\n c.get(\"outputs\", [{}])[0].get(\"type\", \"Any\") if c.get(\"outputs\") else \"Any\"\n ))\n children_desc.append(f\" [{c.get('name', '')}] {c.get('purpose', '')}\\n Signature: {sig}\\n Behavior: {c.get('behavior', '')[:200]}\")\n\n user_prompt = f\"\"\"Check if this parent can be implemented by composing its children.\n\nParent: {node.name}\nPurpose: {node.purpose}\nInputs: {', '.join(f'{i.name}: {i.type}' for i in node.inputs)}\nOutputs: {', '.join(f'{o.name}: {o.type}' for o in node.outputs)}\n\nChildren:\n{chr(10).join(children_desc)}\n\nReturn ONLY the JSON response.\"\"\"\n\n try:\n raw = logger.chat([\n {\"role\": \"system\", \"content\": COMPOSE_SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_prompt},\n ], max_tokens=1024)\n parsed = parse_json(raw)\n return parsed\n except Exception as e:\n return {\"status\": \"error\", \"error\": str(e)}\n\n\n# ========================================================================\n# Three-stage decomposition\n# ========================================================================\n\ndef build_stage1_user_prompt(node):\n \"\"\"Build user prompt for Stage 1 from a Node object.\"\"\"\n lines = [\n \"Decompose the following function block:\", \"\",\n f\"Node Name: {node.name}\",\n f\"Node Purpose: {node.purpose}\", \"\",\n ]\n if node.subprd and node.subprd.description:\n lines.append(node.subprd.description)\n lines.append(\"\")\n lines.append(\"Inputs:\")\n for inp in node.inputs:\n lines.append(f\" - {inp.name}: {inp.type} - {inp.description}\")\n lines.append(\"Outputs:\")\n for out in node.outputs:\n lines.append(f\" - {out.name}: {out.type} - {out.description}\")\n lines.append(\"\")\n if node.subprd and node.subprd.constraints:\n lines.append(\"Constraints:\")\n for c in node.subprd.constraints:\n if isinstance(c, dict):\n lines.append(f\" - {c.get('description', c)}\")\n else:\n lines.append(f\" - {c}\")\n lines.append(\"\")\n if node.data_sources:\n lines.append(\"Available Data Stores:\")\n for ds in node.data_sources:\n lines.append(f\" - {ds.name} ({ds.category}, {ds.access}): {ds.description}\")\n lines.append(\"\")\n if node.global_vars:\n lines.append(\"Global Variables:\")\n for gv in node.global_vars:\n lines.append(f\" - {gv.op} on {gv.variable}: {gv.description}\")\n lines.append(\"\")\n lines.append(\"Maximum children allowed: 10\")\n lines.append(\"Maximum depth: 3\")\n lines.append(\"\")\n lines.append(\"Return ONLY the JSON response.\")\n return \"\\n\".join(lines)\n\n\ndef build_stage2_user_prompt(stage1_data, node):\n children = stage1_data.get(\"children\", [])\n lines = [\n \"Derive interfaces for each child.\", \"\",\n f\"Parent: {node.name}\",\n f\"Purpose: {node.purpose}\", \"\",\n \"Inputs:\",\n ]\n for inp in node.inputs:\n lines.append(f\" - {inp.name}: {inp.type} - {inp.description}\")\n lines.append(\"Outputs:\")\n for out in node.outputs:\n lines.append(f\" - {out.name}: {out.type} - {out.description}\")\n lines.append(\"\")\n if node.data_sources:\n lines.append(\"Available Data Stores:\")\n for ds in node.data_sources:\n lines.append(f\" - {ds.name} ({ds.category}, {ds.access})\")\n lines.append(\"\")\n lines.append(\"Children (frozen from Stage 1):\")\n for c in children:\n lines.append(f\" {c.get('name', '')}: {c.get('purpose', '')}\")\n lines.append(f\" behavior: {c.get('behavior', '')[:200]}\")\n lines.append(f\" role: {c.get('composition_role', '')}\")\n lines.append(\"\")\n lines.append(\"Return ONLY the JSON response.\")\n return \"\\n\".join(lines)\n\n\ndef build_stage3_user_prompt(stage1_data, stage2_data, node):\n children = stage2_data.get(\"children\", stage1_data.get(\"children\", []))\n lines = [\n \"Derive governance and resource fields for each child.\", \"\",\n f\"Parent: {node.name}\",\n f\"Purpose: {node.purpose}\", \"\",\n ]\n if node.data_sources:\n lines.append(\"Available Data Stores:\")\n for ds in node.data_sources:\n lines.append(f\" - {ds.name} ({ds.category}, {ds.access})\")\n lines.append(\"\")\n if node.global_vars:\n lines.append(\"=== PARENT GLOBAL STATE CONSERVATION LEDGER ===\")\n lines.append(\"Every row below MUST be covered by the union of child global_vars.\")\n lines.append(\"Do not drop any row. If a row requires read_write, both read and write must appear.\")\n lines.append(\"\")\n lines.append(\"| Variable | Required Op | Description |\")\n lines.append(\"|----------|-------------|-------------|\")\n for gv in node.global_vars:\n lines.append(f\"| {gv.variable} | {gv.op} | {gv.description} |\")\n lines.append(\"\")\n lines.append(\"After assigning child global_vars, verify every row above is covered.\")\n lines.append(\"\")\n lines.append(\"Children (frozen from Stage 1 + Stage 2):\")\n for c in children:\n lines.append(f\" {c.get('name', '')}: {c.get('purpose', '')}\")\n sig = c.get(\"signature\", \"\")\n if sig:\n lines.append(f\" signature: {sig}\")\n lines.append(\"\")\n lines.append(\"Return ONLY the JSON response.\")\n return \"\\n\".join(lines)\n\n\ndef merge_stages(stage1, stage2, stage3):\n \"\"\"Merge three stages into a single children list compatible with pipeline checks.\"\"\"\n s1_children = {c.get(\"name\", \"\"): c for c in stage1.get(\"children\", [])}\n s2_children = {c.get(\"name\", \"\"): c for c in stage2.get(\"children\", [])}\n s3_children = {c.get(\"name\", \"\"): c for c in stage3.get(\"children\", [])}\n\n merged = []\n for name in s1_children:\n m = {}\n # Stage 1 fields\n m.update(s1_children[name])\n # Stage 2 fields (override/add)\n if name in s2_children:\n s2 = s2_children[name]\n for key in (\"inputs\", \"outputs\", \"signature\"):\n if key in s2:\n m[key] = s2[key]\n # Stage 3 fields (override/add)\n if name in s3_children:\n s3 = s3_children[name]\n for key in (\"global_vars\", \"data_operations\", \"requested_capabilities\",\n \"constraints\", \"acceptance_criteria\", \"traceability\", \"node_type\"):\n if key in s3:\n m[key] = s3[key]\n merged.append(m)\n return merged\n\n\n# ========================================================================\n# Trial runner\n# ========================================================================\n\ndef _fix_constraints(node):\n \"\"\"Convert string constraints to dict format expected by real pipeline decomposer.\"\"\"\n import copy\n node = copy.deepcopy(node)\n if node.subprd and node.subprd.constraints:\n fixed = []\n for c in node.subprd.constraints:\n if isinstance(c, str):\n fixed.append({\"description\": c})\n elif isinstance(c, dict):\n fixed.append(c)\n else:\n fixed.append({\"description\": str(c)})\n node.subprd.constraints = fixed\n return node\n\n\ndef run_trial_baseline(case, trial_idx, api_key, base_url, model):\n \"\"\"Run single-stage baseline decomposition.\"\"\"\n node = _fix_constraints(case[\"node\"])\n case_name = node.name\n label = f\"single_stage_baseline/{case_name}/trial_{trial_idx:02d}\"\n log_dir = os.path.join(OUTPUT_DIR, model, \"single_stage_baseline\", case_name, f\"trial_{trial_idx:02d}\")\n logger = LLMLogger(log_dir, api_key, base_url, model)\n t0 = time.time()\n\n # Use real pipeline decomposer prompt\n config = PipelineConfig(api_key=\"dummy\", max_children=10, max_depth=3)\n decomposer = Decomposer(config, None)\n system_prompt = decomposer._build_system_prompt()\n user_prompt = decomposer._build_user_prompt(node)\n\n try:\n raw = logger.chat([\n {\"role\": \"system\", \"content\": system_prompt},\n {\"role\": \"user\", \"content\": user_prompt},\n ])\n except Exception as e:\n return {\"label\": label, \"condition\": \"single_stage_baseline\", \"case\": case_name,\n \"trial\": trial_idx, \"error\": f\"API failed: {e}\", \"elapsed\": time.time()-t0, \"llm_calls\": 1}\n\n parsed = parse_json(raw)\n elapsed = round(time.time()-t0, 1)\n\n children = parsed.get(\"children\", [])\n has_routing, sibling_calls = detect_routing(children)\n\n # Deterministic checks\n child_count_viol = check_child_count(children, case.get(\"expected_children_range\", (2, 10)))\n missing_fields = check_missing_required_fields(children, \"single_stage_baseline\")\n dangling = check_dangling_inputs(children, node)\n gv_subset = check_global_var_subset(children, node.global_vars)\n gv_gap = check_global_var_union_gap(children, node.global_vars)\n\n # Composition check\n comp_result = check_cannot_compose(node, children, logger)\n cannot_compose = comp_result.get(\"status\") == \"cannot_compose\"\n\n result = {\n \"label\": label, \"condition\": \"single_stage_baseline\", \"case\": case_name,\n \"trial\": trial_idx, \"n_children\": len(children),\n \"child_names\": [c.get(\"name\", \"\") for c in children],\n \"has_routing\": has_routing, \"sibling_calls\": sibling_calls,\n \"child_count_violation\": child_count_viol,\n \"missing_required_fields\": missing_fields[:10],\n \"dangling_inputs\": dangling[:10],\n \"global_var_subset_violations\": gv_subset[:10],\n \"global_var_union_gap\": gv_gap[:10],\n \"cannot_compose\": cannot_compose,\n \"composition_feedback\": comp_result.get(\"decomposition_feedback\", {}),\n \"parse_error\": \"error\" in parsed and not parsed.get(\"children\"),\n \"elapsed\": elapsed, \"llm_calls\": logger.call_counter,\n }\n\n with open(os.path.join(log_dir, \"result.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(result, f, indent=2, ensure_ascii=False, default=str)\n return result\n\n\ndef run_trial_notraditional(case, trial_idx, api_key, base_url, model):\n \"\"\"Run single-stage with notraditional addition.\"\"\"\n node = _fix_constraints(case[\"node\"])\n case_name = node.name\n label = f\"single_stage_notraditional/{case_name}/trial_{trial_idx:02d}\"\n log_dir = os.path.join(OUTPUT_DIR, model, \"single_stage_notraditional\", case_name, f\"trial_{trial_idx:02d}\")\n logger = LLMLogger(log_dir, api_key, base_url, model)\n t0 = time.time()\n\n config = PipelineConfig(api_key=\"dummy\", max_children=10, max_depth=3)\n decomposer = Decomposer(config, None)\n system_prompt = decomposer._build_system_prompt() + NOTRADITIONAL_ADDITION\n user_prompt = decomposer._build_user_prompt(node)\n\n try:\n raw = logger.chat([\n {\"role\": \"system\", \"content\": system_prompt},\n {\"role\": \"user\", \"content\": user_prompt},\n ])\n except Exception as e:\n return {\"label\": label, \"condition\": \"single_stage_notraditional\", \"case\": case_name,\n \"trial\": trial_idx, \"error\": f\"API failed: {e}\", \"elapsed\": time.time()-t0, \"llm_calls\": 1}\n\n parsed = parse_json(raw)\n elapsed = round(time.time()-t0, 1)\n\n children = parsed.get(\"children\", [])\n has_routing, sibling_calls = detect_routing(children)\n\n child_count_viol = check_child_count(children, case.get(\"expected_children_range\", (2, 10)))\n missing_fields = check_missing_required_fields(children, \"single_stage_notraditional\")\n dangling = check_dangling_inputs(children, node)\n gv_subset = check_global_var_subset(children, node.global_vars)\n gv_gap = check_global_var_union_gap(children, node.global_vars)\n\n comp_result = check_cannot_compose(node, children, logger)\n cannot_compose = comp_result.get(\"status\") == \"cannot_compose\"\n\n result = {\n \"label\": label, \"condition\": \"single_stage_notraditional\", \"case\": case_name,\n \"trial\": trial_idx, \"n_children\": len(children),\n \"child_names\": [c.get(\"name\", \"\") for c in children],\n \"has_routing\": has_routing, \"sibling_calls\": sibling_calls,\n \"child_count_violation\": child_count_viol,\n \"missing_required_fields\": missing_fields[:10],\n \"dangling_inputs\": dangling[:10],\n \"global_var_subset_violations\": gv_subset[:10],\n \"global_var_union_gap\": gv_gap[:10],\n \"cannot_compose\": cannot_compose,\n \"composition_feedback\": comp_result.get(\"decomposition_feedback\", {}),\n \"parse_error\": \"error\" in parsed and not parsed.get(\"children\"),\n \"elapsed\": elapsed, \"llm_calls\": logger.call_counter,\n }\n\n with open(os.path.join(log_dir, \"result.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(result, f, indent=2, ensure_ascii=False, default=str)\n return result\n\n\ndef run_trial_threestage(case, trial_idx, api_key, base_url, model):\n \"\"\"Run three-stage decomposition.\"\"\"\n node = _fix_constraints(case[\"node\"])\n case_name = node.name\n label = f\"three_stage/{case_name}/trial_{trial_idx:02d}\"\n log_dir = os.path.join(OUTPUT_DIR, model, \"three_stage\", case_name, f\"trial_{trial_idx:02d}\")\n logger = LLMLogger(log_dir, api_key, base_url, model)\n t0 = time.time()\n total_llm = 0\n\n # Stage 1\n try:\n s1_raw = logger.chat([\n {\"role\": \"system\", \"content\": STAGE1_SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": build_stage1_user_prompt(node)},\n ])\n except Exception as e:\n return {\"label\": label, \"condition\": \"three_stage\", \"case\": case_name,\n \"trial\": trial_idx, \"error\": f\"Stage1 API failed: {e}\", \"elapsed\": time.time()-t0, \"llm_calls\": 1}\n\n stage1 = parse_json(s1_raw)\n total_llm = logger.call_counter\n\n if \"error\" in stage1 and not stage1.get(\"children\"):\n return {\"label\": label, \"condition\": \"three_stage\", \"case\": case_name,\n \"trial\": trial_idx, \"error\": f\"Stage1 parse: {stage1.get('error')}\",\n \"elapsed\": round(time.time()-t0, 1), \"llm_calls\": total_llm}\n\n with open(os.path.join(log_dir, \"stage1.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(stage1, f, indent=2, ensure_ascii=False)\n\n # Stage 2\n try:\n s2_raw = logger.chat([\n {\"role\": \"system\", \"content\": STAGE2_SYSTEM_PROMPT},\n {\"role\": \"assistant\", \"content\": json.dumps(stage1, indent=2, ensure_ascii=False)},\n {\"role\": \"user\", \"content\": build_stage2_user_prompt(stage1, node)},\n ])\n except Exception as e:\n return {\"label\": label, \"condition\": \"three_stage\", \"case\": case_name,\n \"trial\": trial_idx, \"error\": f\"Stage2 API failed: {e}\",\n \"elapsed\": round(time.time()-t0, 1), \"llm_calls\": total_llm}\n\n stage2 = parse_json(s2_raw)\n total_llm = logger.call_counter\n\n with open(os.path.join(log_dir, \"stage2.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(stage2, f, indent=2, ensure_ascii=False)\n\n # Stage 3\n try:\n s3_raw = logger.chat([\n {\"role\": \"system\", \"content\": STAGE3_SYSTEM_PROMPT},\n {\"role\": \"assistant\", \"content\": json.dumps(stage2, indent=2, ensure_ascii=False)},\n {\"role\": \"user\", \"content\": build_stage3_user_prompt(stage1, stage2, node)},\n ])\n except Exception as e:\n return {\"label\": label, \"condition\": \"three_stage\", \"case\": case_name,\n \"trial\": trial_idx, \"error\": f\"Stage3 API failed: {e}\",\n \"elapsed\": round(time.time()-t0, 1), \"llm_calls\": total_llm}\n\n stage3 = parse_json(s3_raw)\n total_llm = logger.call_counter\n\n with open(os.path.join(log_dir, \"stage3.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(stage3, f, indent=2, ensure_ascii=False)\n\n # Merge stages\n merged = merge_stages(stage1, stage2, stage3)\n with open(os.path.join(log_dir, \"merged_node.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(merged, f, indent=2, ensure_ascii=False)\n\n elapsed = round(time.time()-t0, 1)\n\n # Run checks on merged result\n has_routing, sibling_calls = detect_routing(merged)\n child_count_viol = check_child_count(merged, case.get(\"expected_children_range\", (2, 10)))\n missing_fields = check_missing_required_fields(merged, \"three_stage\")\n dangling = check_dangling_inputs(merged, node)\n gv_subset = check_global_var_subset(merged, node.global_vars)\n gv_gap = check_global_var_union_gap(merged, node.global_vars)\n\n # Check stage drift\n s1_names = [c.get(\"name\", \"\") for c in stage1.get(\"children\", [])]\n merged_names = [c.get(\"name\", \"\") for c in merged]\n stage_drift = s1_names != merged_names\n\n # Composition check\n comp_result = check_cannot_compose(node, merged, logger)\n cannot_compose = comp_result.get(\"status\") == \"cannot_compose\"\n total_llm = logger.call_counter\n\n result = {\n \"label\": label, \"condition\": \"three_stage\", \"case\": case_name,\n \"trial\": trial_idx, \"n_children\": len(merged),\n \"child_names\": merged_names,\n \"has_routing\": has_routing, \"sibling_calls\": sibling_calls,\n \"child_count_violation\": child_count_viol,\n \"missing_required_fields\": missing_fields[:10],\n \"dangling_inputs\": dangling[:10],\n \"global_var_subset_violations\": gv_subset[:10],\n \"global_var_union_gap\": gv_gap[:10],\n \"stage_drift\": stage_drift,\n \"cannot_compose\": cannot_compose,\n \"composition_feedback\": comp_result.get(\"decomposition_feedback\", {}),\n \"parse_error\": False,\n \"elapsed\": elapsed, \"llm_calls\": total_llm,\n }\n\n with open(os.path.join(log_dir, \"result.json\"), \"w\", encoding=\"utf-8\") as f:\n json.dump(result, f, indent=2, ensure_ascii=False, default=str)\n return result\n\n\n# ========================================================================\n# Report generation\n# ========================================================================\n\ndef generate_report(all_results, model, cases, n_trials):\n lines = [\n \"# Exp03: Shallow Pipeline Regression Report\",\n \"\",\n f\"Model: `{model}`\",\n f\"Cases: {', '.join(c['node'].name for c in cases)}\",\n f\"Trials per condition per case: {n_trials}\",\n f\"Conditions: single_stage_baseline, single_stage_notraditional, three_stage\",\n f\"\",\n ]\n\n conditions = [\"single_stage_baseline\", \"single_stage_notraditional\", \"three_stage\"]\n metrics = [\"routing_rate\", \"child_count_violation_rate\", \"missing_field_rate\",\n \"dangling_input_rate\", \"global_var_subset_violation_rate\",\n \"global_var_union_gap_rate\", \"cannot_compose_rate\", \"parse_error_rate\"]\n\n # Results matrix\n lines.append(\"## Results Matrix (Condition x Metric)\\n\")\n header = \"| Condition | Trials | \" + \" | \".join(m.replace(\"_rate\", \"\") for m in metrics) + \" |\"\n sep = \"|-----------|--------|\" + \"|\".join([\"---\"] * len(metrics)) + \"|\"\n lines.append(header)\n lines.append(sep)\n\n for cond in conditions:\n results = [r for r in all_results if r.get(\"condition\") == cond]\n n = len(results)\n if n == 0:\n continue\n vals = []\n vals.append(f\"{sum(1 for r in results if r.get('has_routing', False))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('child_count_violation', False))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('missing_required_fields'))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('dangling_inputs'))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('global_var_subset_violations'))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('global_var_union_gap'))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('cannot_compose', False))}/{n}\")\n vals.append(f\"{sum(1 for r in results if r.get('parse_error', False) or r.get('error'))}/{n}\")\n lines.append(f\"| {cond} | {n} | \" + \" | \".join(vals) + \" |\")\n\n lines.append(\"\")\n\n # Per-case breakdown\n lines.append(\"## Per-Case Breakdown\\n\")\n case_names = sorted(set(r.get(\"case\", \"\") for r in all_results))\n for case_name in case_names:\n lines.append(f\"### {case_name}\\n\")\n lines.append(\"| Condition | Routing | CC Viol | Missing | Dangling | GV Subset | GV Gap | Compose |\")\n lines.append(\"|-----------|---------|---------|---------|----------|-----------|--------|---------|\")\n for cond in conditions:\n results = [r for r in all_results if r.get(\"condition\") == cond and r.get(\"case\") == case_name]\n n = len(results)\n if n == 0:\n continue\n routing = sum(1 for r in results if r.get(\"has_routing\", False))\n cc = sum(1 for r in results if r.get(\"child_count_violation\", False))\n miss = sum(1 for r in results if r.get(\"missing_required_fields\"))\n dang = sum(1 for r in results if r.get(\"dangling_inputs\"))\n gv_sub = sum(1 for r in results if r.get(\"global_var_subset_violations\"))\n gv_gap = sum(1 for r in results if r.get(\"global_var_union_gap\"))\n comp = sum(1 for r in results if r.get(\"cannot_compose\", False))\n lines.append(f\"| {cond} | {routing}/{n} | {cc}/{n} | {miss}/{n} | {dang}/{n} | {gv_sub}/{n} | {gv_gap}/{n} | {comp}/{n} |\")\n lines.append(\"\")\n\n # Top failures\n lines.append(\"## Top Failure Reasons\\n\")\n failure_counter = defaultdict(int)\n for r in all_results:\n if r.get(\"has_routing\"):\n failure_counter[\"routing\"] += 1\n if r.get(\"child_count_violation\"):\n failure_counter[\"child_count_violation\"] += 1\n if r.get(\"missing_required_fields\"):\n failure_counter[\"missing_required_fields\"] += 1\n if r.get(\"dangling_inputs\"):\n failure_counter[\"dangling_inputs\"] += 1\n if r.get(\"global_var_subset_violations\"):\n failure_counter[\"global_var_subset_violations\"] += 1\n if r.get(\"global_var_union_gap\"):\n failure_counter[\"global_var_union_gap\"] += 1\n if r.get(\"cannot_compose\"):\n failure_counter[\"cannot_compose\"] += 1\n if r.get(\"parse_error\") or r.get(\"error\"):\n failure_counter[\"parse_error\"] += 1\n for reason, count in sorted(failure_counter.items(), key=lambda x: -x[1]):\n lines.append(f\"- {reason}: {count} occurrences\")\n lines.append(\"\")\n\n # Representative failures\n lines.append(\"## Representative Failures\\n\")\n failure_cases = [r for r in all_results if any([\n r.get(\"has_routing\"), r.get(\"cannot_compose\"), r.get(\"global_var_union_gap\"),\n r.get(\"dangling_inputs\"),\n ])]\n for r in failure_cases[:5]:\n lines.append(f\"- **{r['label']}**: routing={r.get('has_routing')}, compose={r.get('cannot_compose')}, \"\n f\"gv_gap={len(r.get('global_var_union_gap', []))}, dangling={len(r.get('dangling_inputs', []))}\")\n lines.append(\"\")\n\n # Verdict\n lines.append(\"## Verdict\\n\")\n baseline_routing = sum(1 for r in all_results if r.get(\"condition\") == \"single_stage_baseline\" and r.get(\"has_routing\"))\n notrad_routing = sum(1 for r in all_results if r.get(\"condition\") == \"single_stage_notraditional\" and r.get(\"has_routing\"))\n three_routing = sum(1 for r in all_results if r.get(\"condition\") == \"three_stage\" and r.get(\"has_routing\"))\n baseline_n = sum(1 for r in all_results if r.get(\"condition\") == \"single_stage_baseline\")\n notrad_n = sum(1 for r in all_results if r.get(\"condition\") == \"single_stage_notraditional\")\n three_n = sum(1 for r in all_results if r.get(\"condition\") == \"three_stage\")\n\n lines.append(f\"- Baseline routing: {baseline_routing}/{baseline_n}\")\n lines.append(f\"- Notraditional routing: {notrad_routing}/{notrad_n}\")\n lines.append(f\"- Three-stage routing: {three_routing}/{three_n}\")\n lines.append(\"\")\n\n # Check if three_stage improves on notraditional\n three_gap = sum(len(r.get(\"global_var_union_gap\", [])) for r in all_results if r.get(\"condition\") == \"three_stage\")\n three_dang = sum(len(r.get(\"dangling_inputs\", [])) for r in all_results if r.get(\"condition\") == \"three_stage\")\n three_comp = sum(1 for r in all_results if r.get(\"condition\") == \"three_stage\" and r.get(\"cannot_compose\"))\n notrad_gap = sum(len(r.get(\"global_var_union_gap\", [])) for r in all_results if r.get(\"condition\") == \"single_stage_notraditional\")\n notrad_dang = sum(len(r.get(\"dangling_inputs\", [])) for r in all_results if r.get(\"condition\") == \"single_stage_notraditional\")\n notrad_comp = sum(1 for r in all_results if r.get(\"condition\") == \"single_stage_notraditional\" and r.get(\"cannot_compose\"))\n\n lines.append(f\"- GV union gap: notraditional={notrad_gap}, three_stage={three_gap}\")\n lines.append(f\"- Dangling inputs: notraditional={notrad_dang}, three_stage={three_dang}\")\n lines.append(f\"- Cannot compose: notraditional={notrad_comp}, three_stage={three_comp}\")\n lines.append(\"\")\n\n if three_routing <= notrad_routing and (three_gap < notrad_gap or three_dang < notrad_dang or three_comp < notrad_comp):\n verdict = \"PASS\"\n elif three_routing > notrad_routing:\n verdict = \"FAIL (routing regression)\"\n else:\n verdict = \"INCONCLUSIVE\"\n lines.append(f\"- **Verdict: {verdict}**\")\n lines.append(\"\")\n return \"\\n\".join(lines)\n\n\n# ========================================================================\n# Main\n# ========================================================================\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Exp03: Shallow Pipeline Regression\")\n parser.add_argument(\"--model\", type=str, default=None)\n parser.add_argument(\"--trials\", type=int, default=3)\n parser.add_argument(\"--cases\", type=str, default=\"OrderSystem,ChatApp,PatientPortal,BuildSystem,DataPipeline\",\n help=\"Comma-separated case names\")\n parser.add_argument(\"--base_url\", type=str, default=None)\n parser.add_argument(\"--api_key\", type=str, default=None)\n args = parser.parse_args()\n\n model = args.model or _env(\"CHRONOS_MODEL\", \"deepseek-chat\")\n if model in {\"mimo-v2.5\", \"mimo-v2-flash\", \"mimo-v2.5-pro\", \"mimo-v2-pro\", \"mimo-v2-omni\"}:\n base_url = args.base_url or os.getenv(\"MIMO_BASE_URL\", \"https://api.xiaomimimo.com/v1\")\n api_key = args.api_key or os.getenv(\"MIMO_API_KEY\") or _env(\"CHRONOS_API_KEY\")\n else:\n base_url = args.base_url or _env(\"CHRONOS_BASE_URL\", \"https://api.deepseek.com\")\n api_key = args.api_key or _env(\"CHRONOS_API_KEY\")\n\n if not api_key:\n print(\"ERROR: Set CHRONOS_API_KEY\"); return 1\n\n # Select cases\n case_name_map = {c[\"node\"].name: c for c in ALL_CASES}\n requested = [c.strip() for c in args.cases.split(\",\")]\n cases = [case_name_map[c] for c in requested if c in case_name_map]\n if not cases:\n print(f\"ERROR: No valid cases. Available: {list(case_name_map.keys())}\"); return 1\n\n print(f\"Model: {model}\")\n print(f\"Cases: {[c['node'].name for c in cases]}\")\n print(f\"Trials per condition per case: {args.trials}\")\n print(f\"Conditions: single_stage_baseline, single_stage_notraditional, three_stage\")\n print(f\"Output: {OUTPUT_DIR}/{model}/\")\n print()\n\n # Build task list\n tasks = []\n for case in cases:\n for t in range(args.trials):\n tasks.append((\"single_stage_baseline\", case, t))\n tasks.append((\"single_stage_notraditional\", case, t))\n tasks.append((\"three_stage\", case, t))\n\n all_results = []\n with ThreadPoolExecutor(max_workers=MAX_CONCURRENCY) as pool:\n futures = {}\n for cond, case, t in tasks:\n case_name = case[\"node\"].name\n if cond == \"single_stage_baseline\":\n f = pool.submit(run_trial_baseline, case, t, api_key, base_url, model)\n elif cond == \"single_stage_notraditional\":\n f = pool.submit(run_trial_notraditional, case, t, api_key, base_url, model)\n else:\n f = pool.submit(run_trial_threestage, case, t, api_key, base_url, model)\n futures[f] = f\"{cond}/{case_name}_{t:02d}\"\n\n for f in as_completed(futures):\n r = f.result()\n all_results.append(r)\n err = r.get(\"error\", \"\")\n if err:\n print(f\" [{r['label']}] ERROR: {err[:80]}\")\n else:\n rt = \"RT\" if r.get(\"has_routing\") else \"ok\"\n cc = \"CC!\" if r.get(\"child_count_violation\") else \"\"\n comp = \"COMP!\" if r.get(\"cannot_compose\") else \"\"\n print(f\" [{r['label']}] {r.get('n_children',0)}ch {rt} {cc} {comp} {r.get('elapsed',0)}s\")\n\n # Sort results\n all_results.sort(key=lambda r: (r.get(\"condition\", \"\"), r.get(\"case\", \"\"), r.get(\"trial\", 0)))\n\n # Save results\n out_dir = os.path.join(OUTPUT_DIR, model)\n os.makedirs(out_dir, exist_ok=True)\n results_path = os.path.join(out_dir, \"results.json\")\n with open(results_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(all_results, f, indent=2, ensure_ascii=False, default=str)\n print(f\"\\nSaved: {results_path}\")\n\n # Generate report\n report = generate_report(all_results, model, cases, args.trials)\n report_path = os.path.join(out_dir, \"report.md\")\n with open(report_path, \"w\", encoding=\"utf-8\") as f:\n f.write(report)\n print(f\"Saved: {report_path}\")\n\n # Summary\n total = len(all_results)\n total_routing = sum(1 for r in all_results if r.get(\"has_routing\", False))\n total_comp = sum(1 for r in all_results if r.get(\"cannot_compose\", False))\n total_err = sum(1 for r in all_results if r.get(\"error\") or r.get(\"parse_error\", False))\n print(f\"\\n{'='*60}\")\n print(f\" SUMMARY\")\n print(f\"{'='*60}\")\n print(f\" Total trials: {total}\")\n print(f\" Routing: {total_routing}/{total}\")\n print(f\" Cannot compose: {total_comp}/{total}\")\n print(f\" Errors: {total_err}/{total}\")\n print(f\"{'='*60}\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n"}}}} |