Files
rune/scripts/research_inward_context.py
T
Nick f64ee95aad
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / best-practices (push) Successful in 27s
Auto-label / auto-label (pull_request) Successful in 7s
CI Pipeline / documentation (push) Successful in 9s
CI Pipeline / canary (push) Successful in 54s
CI Pipeline / lint (push) Successful in 1m9s
CI Pipeline / type-check (push) Failing after 38s
CI Pipeline / gitea-offline (push) Successful in 28s
CI Pipeline / best-practices (pull_request) Successful in 17s
CI Pipeline / documentation (pull_request) Successful in 12s
CI Pipeline / canary (pull_request) Successful in 47s
CI Pipeline / gitea-offline (pull_request) Successful in 33s
CI Pipeline / type-check (pull_request) Failing after 55s
deploy / deploy (push) Skipped
Spec gate (headless) / spec-gate (pull_request) Successful in 23s
CI Pipeline / unit-tests (push) Failing after 3m40s
CI Pipeline / required-checks (push) Failing after 9s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 19s
CI Pipeline / lint (pull_request) Successful in 1m4s
CI Pipeline / unit-tests (pull_request) Failing after 3m38s
CI Pipeline / required-checks (pull_request) Failing after 8s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 13s
feat(t21.5): root-cause continuation + guard retirement + V1 parity
- ci_event_wait: OPERATION_SPECS (op-specific completion authority:
  branch_ci/pull_request_checks -> own runs; master_ci/deploy/release ->
  deploy qualification); SINGLE LOGICAL AWAIT (all nonterminal conditions
  re-arm internally; model sees only terminal verdicts or real deadline)
- await-operation tool: op_type parameter
- runtime wait guard + cwd guard RETIRED (canonical hook = documented
  no-op; prevention lives in tests, not interception)
- wr_root_resolver.py: ONE wait/resume root authority; split-brain fails
  loudly; start_wr_daemon.py resolves root once; governed reconciliation
- construction_activation._auto_structural_awareness: PROJECT_BUILD
  bootstrap auto-registers Registry + spine manages edge (no manual
  bookkeeping); fixed latent dispatch_from_decision mode-object bug
- research_memory_port.py: future-facing Memory port inside existing
  INWARD (provider contract, zero-result fast path, teachable-moment
  candidate seam; APPLICABILITY governs transfer)
- v1_parity_manifest.py: 56 rows, 0 blocking mandatory-V1 items
- G1-G12 gate evidence in store/scaffold/t21.5_gates.json
2026-09-17 12:56:52 +01:00

160 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""research_inward_context.py — deterministic INWARD context builder (R2).
Builds a bounded candidate context for MiMo INWARD inspection:
1. Issue/component metadata from the issue body
2. Architecture authority map (umbrella manifest)
3. File matches from issue keywords against the codebase
4. Test files matching the component
This is DETERMINISTIC — no MiMo needed. MiMo gets the candidate context
and only performs semantic inspection if the context requires it.
"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
REPO_ROOT = os.environ.get("REPO_ROOT", ".")
UMBRELLA_MANIFEST = os.path.join(REPO_ROOT, "config", "umbrellas.json")
def build_context(issue: dict, define_contract: dict) -> dict:
"""Build a bounded candidate context for the INWARD stage."""
title = issue.get("title", "").lower()
body = issue.get("body", "").lower()
keywords = _extract_keywords(title + " " + body)
# 1. Match against the umbrella manifest
manifest = _load_manifest()
matched_modules = _match_modules(keywords, manifest)
# 2. Find matching source files
matched_files = _find_files(keywords)
# 3. Find matching test files
matched_tests = _find_tests(keywords)
# 4. T21.5 §10: Memory internal-experience port — ONE port of call,
# bounded, zero-result = normal (fast path: [] → next step directly).
from research_memory_port import memory_port_of_call
memory_candidates = memory_port_of_call(issue, keywords)
return {
"keywords": keywords,
"umbrella_matches": matched_modules,
"candidate_files": matched_files[:15], # bounded
"candidate_tests": matched_tests[:10], # bounded
"memory_candidates": memory_candidates, # evidence, never authority
"define_contract": define_contract,
"mimo_needed": _needs_mimo(matched_files, matched_tests),
}
def _extract_keywords(text: str) -> list[str]:
"""Extract meaningful keywords from the issue text."""
stop = {
"the",
"a",
"an",
"in",
"for",
"to",
"of",
"and",
"or",
"is",
"are",
"when",
"how",
"what",
"this",
"that",
"with",
"on",
"at",
"from",
}
words = re.findall(r"[a-z_]{3,}", text.lower())
return [w for w in words if w not in stop][:20]
def _load_manifest() -> dict:
"""Structural facts from SPINE (the single structural authority).
Phase D: reads the Spine-generated compatibility view (same schema as
the old manual manifest). The view is OUTPUT — the authority is
store/spine/current.json; direct edits to the view are flagged by
`spine drift`. If a manual manifest is present (Spine not yet seeded
in this checkout), it degrades gracefully to that legacy truth."""
if os.path.exists(UMBRELLA_MANIFEST):
with open(UMBRELLA_MANIFEST) as f:
data = json.load(f)
return data # generated view or manual manifest — both work
return {"umbrellas": {}}
def _match_modules(keywords: list[str], manifest: dict) -> list[dict]:
"""Match keywords against umbrella manifest module names."""
matches = []
for uname, udata in manifest.get("umbrellas", {}).items():
for module in udata.get("modules", []):
for kw in keywords:
if kw in module.lower():
matches.append({"umbrella": uname, "module": module, "keyword": kw})
break
return matches
def _find_files(keywords: list[str]) -> list[str]:
"""Find source files matching the keywords."""
src_dir = os.path.join(REPO_ROOT, "src", "rune")
matches = []
if not os.path.isdir(src_dir):
return matches
for fname in sorted(os.listdir(src_dir)):
if not fname.endswith(".py"):
continue
fpath = os.path.join(src_dir, fname)
try:
content = open(fpath, errors="replace").read().lower()
except Exception:
continue
score = sum(1 for kw in keywords if kw in content)
if score > 0:
matches.append({"path": f"src/rune/{fname}", "score": score})
matches.sort(key=lambda x: -x["score"])
return [m["path"] for m in matches]
def _find_tests(keywords: list[str]) -> list[str]:
"""Find test files matching the keywords."""
test_dir = os.path.join(REPO_ROOT, "tests")
matches = []
if not os.path.isdir(test_dir):
return matches
for fname in sorted(os.listdir(test_dir)):
if not fname.startswith("test_") or not fname.endswith(".py"):
continue
fpath = os.path.join(test_dir, fname)
try:
content = open(fpath, errors="replace").read().lower()
except Exception:
continue
score = sum(1 for kw in keywords if kw in content)
if score > 0:
matches.append({"path": f"tests/{fname}", "score": score})
matches.sort(key=lambda x: -x["score"])
return [m["path"] for m in matches]
def _needs_mimo(files: list, tests: list) -> bool:
"""MiMo is needed if the deterministic context is insufficient to
identify the exact code area. If we have clear file matches, MiMo
gets a bounded prompt over those files. If no matches, MiMo needs
to explore."""
return len(files) == 0