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
- 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
160 lines
5.3 KiB
Python
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
|