feat: enforce hard test-tier requirement across all DevPlace workflow agents and feature-builder docs

Update the feature-builder agent prompt, test-maintainer agent, and all four workflow JS files (devii-tool, endpoint, feature, job-service) to codify the DevPlace test standard as a non-optional project requirement: one test file per endpoint, directory tree mirroring the URL/source path, split into three tiers (unit, api, e2e). Add explicit Test phases to devii-tool, endpoint, feature, and job-service workflows, and embed tier-specific test instructions (path mapping, fixture choice, coverage scope) directly in each workflow's meta description and TESTS constant.
This commit is contained in:
2026-06-15 12:10:14 +00:00
parent 3c7527988e
commit e1874f9b6a
71 changed files with 2198 additions and 145 deletions
+70 -8
View File
@@ -6,7 +6,8 @@ import hashlib
import logging
import math
import re
from dataclasses import dataclass
import time
from dataclasses import dataclass, field
from devplacepy import stealth
from devplacepy.config import INTERNAL_EMBED_MODEL, INTERNAL_EMBED_URL
@@ -22,6 +23,23 @@ TOKEN_PATTERN = re.compile(r"[a-z0-9]+")
class EmbedResult:
vectors: list[list[float]]
backend: str
latency_ms: int = 0
cache_hits: int = 0
@dataclass
class EmbeddingCache:
store: dict[str, list[float]] = field(default_factory=dict)
def key(self, text: str) -> str:
return hashlib.sha1((text or "").encode("utf-8")).hexdigest()
def get(self, text: str) -> list[float] | None:
return self.store.get(self.key(text))
def put(self, text: str, vector: list[float]) -> None:
if vector:
self.store[self.key(text)] = vector
def _local_vector(text: str) -> list[float]:
@@ -41,19 +59,51 @@ def _local_vector(text: str) -> list[float]:
def local_embed(texts: list[str]) -> EmbedResult:
return EmbedResult(vectors=[_local_vector(text) for text in texts], backend="local")
start = time.monotonic()
vectors = [_local_vector(text) for text in texts]
latency_ms = int((time.monotonic() - start) * 1000)
return EmbedResult(vectors=vectors, backend="local", latency_ms=latency_ms)
async def embed_texts(
texts: list[str], api_key: str, *, gateway_url: str = INTERNAL_EMBED_URL
texts: list[str],
api_key: str,
*,
gateway_url: str = INTERNAL_EMBED_URL,
cache: EmbeddingCache | None = None,
) -> EmbedResult:
if not texts:
return EmbedResult(vectors=[], backend="empty")
cache_hits = 0
pending_index: list[int] = []
pending_text: list[str] = []
resolved: list[list[float] | None] = [None] * len(texts)
if cache is not None:
for index, text in enumerate(texts):
cached_vector = cache.get(text)
if cached_vector is not None:
resolved[index] = cached_vector
cache_hits += 1
else:
pending_index.append(index)
pending_text.append(text)
else:
pending_index = list(range(len(texts)))
pending_text = list(texts)
if not pending_text:
return EmbedResult(
vectors=[vector or [] for vector in resolved],
backend="cache",
latency_ms=0,
cache_hits=cache_hits,
)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {"model": INTERNAL_EMBED_MODEL, "input": texts}
payload = {"model": INTERNAL_EMBED_MODEL, "input": pending_text}
start = time.monotonic()
backend = "gateway"
try:
async with stealth.stealth_async_client(timeout=EMBED_TIMEOUT_SECONDS) as client:
response = await client.post(gateway_url, json=payload, headers=headers)
@@ -61,10 +111,22 @@ async def embed_texts(
raise RuntimeError(f"embed gateway returned {response.status_code}")
data = response.json()
rows = data.get("data") or []
vectors = [row.get("embedding") or [] for row in rows]
if len(vectors) != len(texts) or any(not vector for vector in vectors):
fresh = [row.get("embedding") or [] for row in rows]
if len(fresh) != len(pending_text) or any(not vector for vector in fresh):
raise RuntimeError("embed gateway returned an incomplete response")
return EmbedResult(vectors=vectors, backend="gateway")
except Exception as exc:
logger.warning("deepsearch embedding gateway failed, using local: %s", exc)
return local_embed(texts)
fresh = [_local_vector(text) for text in pending_text]
backend = "local"
latency_ms = int((time.monotonic() - start) * 1000)
for offset, index in enumerate(pending_index):
vector = fresh[offset]
resolved[index] = vector
if cache is not None:
cache.put(pending_text[offset], vector)
return EmbedResult(
vectors=[vector or [] for vector in resolved],
backend=backend,
latency_ms=latency_ms,
cache_hits=cache_hits,
)
+29 -9
View File
@@ -15,8 +15,7 @@ logger = logging.getLogger(__name__)
TOKEN_PATTERN = re.compile(r"[a-z0-9]+")
BM25_K1 = 1.5
BM25_B = 0.75
HYBRID_VECTOR_WEIGHT = 0.6
HYBRID_KEYWORD_WEIGHT = 0.4
RRF_K = 60.0
DEFAULT_TOP_K = 8
CANDIDATE_MULTIPLIER = 4
@@ -182,18 +181,39 @@ class VectorStore:
)
if not candidates:
return []
vector_rank = sorted(
candidates, key=lambda chunk: chunk.score, reverse=True
)
keyword = self.keyword_scores(query, candidates)
vec_max = max((chunk.score for chunk in candidates), default=0.0) or 1.0
kw_max = max(keyword.values(), default=0.0) or 1.0
keyword_rank = sorted(
candidates, key=lambda chunk: keyword.get(chunk.uid, 0.0), reverse=True
)
fused: dict[str, float] = {}
for rank, chunk in enumerate(vector_rank, start=1):
fused[chunk.uid] = fused.get(chunk.uid, 0.0) + 1.0 / (RRF_K + rank)
for rank, chunk in enumerate(keyword_rank, start=1):
fused[chunk.uid] = fused.get(chunk.uid, 0.0) + 1.0 / (RRF_K + rank)
for chunk in candidates:
vec_norm = max(0.0, chunk.score) / vec_max
kw_norm = keyword.get(chunk.uid, 0.0) / kw_max
chunk.score = (
HYBRID_VECTOR_WEIGHT * vec_norm + HYBRID_KEYWORD_WEIGHT * kw_norm
)
chunk.score = fused.get(chunk.uid, 0.0)
candidates.sort(key=lambda chunk: chunk.score, reverse=True)
return candidates[:top_k]
def coverage_analytics(self) -> dict:
chunks = self.all_chunks()
if not chunks:
return {"chunks": 0, "domains": 0, "sources": 0, "avg_chunk_chars": 0}
domains = {chunk.metadata.get("url", "") for chunk in chunks}
domains.discard("")
sources = {chunk.source for chunk in chunks}
sources.discard("")
avg_chars = int(sum(len(chunk.text) for chunk in chunks) / len(chunks))
return {
"chunks": len(chunks),
"domains": len(domains),
"sources": len(sources),
"avg_chunk_chars": avg_chars,
}
def drop(self) -> None:
try:
import chromadb
@@ -197,6 +197,16 @@ ACTIONS: tuple[Action, ...] = (
body("attachment_uids", ATTACHMENTS),
),
),
Action(
name="edit_comment",
method="POST",
path="/comments/edit/{comment_uid}",
summary="Edit the body of one of your own comments",
params=(
path("comment_uid", "Uid of the comment."),
body("content", "New comment body, 3-1000 characters.", required=True),
),
),
Action(
name="delete_comment",
method="POST",
@@ -910,6 +920,20 @@ ACTIONS: tuple[Action, ...] = (
body("description", "Issue description.", required=True),
),
),
Action(
name="planning_report_generate",
method="POST",
path="/issues/planning",
summary="Queue an admin planning report of all open tickets.",
description=(
"Queues a background job that builds a grouped, ordered markdown planning report "
"of every open ticket and returns {uid, status_url}. Poll the status_url until "
"status is 'done', then show the markdown and the download_url. Admin only."
),
params=(),
requires_auth=True,
requires_admin=True,
),
Action(
name="issue_job_status",
method="GET",
+159
View File
@@ -0,0 +1,159 @@
# retoor <retoor@molodetz.nl>
import logging
import httpx
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL
from devplacepy.services.gitea.config import HTTP_TIMEOUT_SECONDS, GiteaConfig
logger = logging.getLogger(__name__)
MAX_TOKENS = 3000
MAX_ISSUES = 50
BODY_EXCERPT_MAX = 600
PLAN_MAX = 80000
UNGROUPED_LABEL = "General"
SYSTEM_PROMPT = (
"You are a senior engineering lead for DevPlace, a social network for software "
"developers. You are given the full list of OPEN issue tickets from a Gitea tracker. "
"Produce a single, clear delivery plan in GitHub-flavored markdown that GROUPS the "
"tickets into logical workstreams and ORDERS them so dependencies and high-impact work "
"come first. Never invent tickets, numbers, or facts that are not in the input.\n\n"
"Return ONLY markdown, using EXACTLY this structure:\n"
"# Open Tickets Planning\n"
"A short paragraph summarising the overall plan and how many tickets it covers.\n\n"
"Then one level-2 heading per group (a workstream or theme). Under each group, an "
"ordered (numbered) list of its tickets. Each list item MUST start with the ticket "
"number as '#N', then the title, then a single sentence explaining the chosen order "
"or the work to do, for example:\n"
"1. #12 Fix the login redirect - blocks every authenticated flow, do first.\n\n"
"End with a level-2 heading '## Suggested Order' giving a flat numbered list of every "
"ticket number in the recommended execution order."
)
def _excerpt(text: str) -> str:
text = (text or "").strip().replace("\r\n", "\n")
if len(text) > BODY_EXCERPT_MAX:
return text[:BODY_EXCERPT_MAX].rstrip() + "..."
return text
def _labels(issue: dict) -> list[str]:
labels = issue.get("labels") or []
names: list[str] = []
for label in labels:
if isinstance(label, dict):
name = str(label.get("name", "")).strip()
else:
name = str(label).strip()
if name:
names.append(name)
return names
def _group_key(issue: dict) -> str:
labels = _labels(issue)
return labels[0] if labels else UNGROUPED_LABEL
def _fallback(issues: list[dict]) -> str:
groups: dict[str, list[dict]] = {}
for issue in issues:
groups.setdefault(_group_key(issue), []).append(issue)
ordered_group_names = sorted(
groups,
key=lambda name: (name == UNGROUPED_LABEL, name.lower()),
)
lines: list[str] = ["# Open Tickets Planning", ""]
lines.append(
f"Deterministic plan covering {len(issues)} open ticket"
f"{'' if len(issues) == 1 else 's'}, grouped by primary label and ordered by "
"ticket number within each group."
)
lines.append("")
flat_order: list[int] = []
for name in ordered_group_names:
members = sorted(groups[name], key=lambda i: int(i.get("number", 0)))
lines.append(f"## {name}")
for position, issue in enumerate(members, start=1):
number = int(issue.get("number", 0))
title = str(issue.get("title", "")).strip() or "Untitled"
excerpt = _excerpt(issue.get("body", ""))
rationale = excerpt or "No description provided."
lines.append(f"{position}. #{number} {title} - {rationale}")
flat_order.append(number)
lines.append("")
lines.append("## Suggested Order")
for position, number in enumerate(flat_order, start=1):
lines.append(f"{position}. #{number}")
lines.append("")
return "\n".join(lines)[:PLAN_MAX]
def _user_message(issues: list[dict]) -> str:
parts: list[str] = ["Open tickets to plan:", ""]
for issue in issues:
number = int(issue.get("number", 0))
title = str(issue.get("title", "")).strip() or "Untitled"
labels = ", ".join(_labels(issue)) or "none"
excerpt = _excerpt(issue.get("body", ""))
parts.append(f"#{number} {title}")
parts.append(f"labels: {labels}")
parts.append(f"description: {excerpt or 'none'}")
parts.append("")
return "\n".join(parts)
async def generate_plan(
issues: list[dict], config: GiteaConfig
) -> tuple[str, bool]:
issues = list(issues)[:MAX_ISSUES]
if not issues:
return "# Open Tickets Planning\n\nThere are no open tickets to plan.\n", False
if not config.ai_enhance:
return _fallback(issues), False
payload = {
"model": config.ai_model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": _user_message(issues)},
],
"max_tokens": MAX_TOKENS,
"temperature": 0.2,
}
headers = {"Content-Type": "application/json"}
if config.ai_key:
headers["Authorization"] = f"Bearer {config.ai_key}"
try:
async with stealth.stealth_async_client(timeout=HTTP_TIMEOUT_SECONDS) as client:
response = await client.post(
INTERNAL_GATEWAY_URL, json=payload, headers=headers
)
response.raise_for_status()
content = (
response.json()
.get("choices", [{}])[0]
.get("message", {})
.get("content", "")
)
except (httpx.HTTPError, ValueError, KeyError, IndexError) as exc:
logger.warning("Planning generation failed, using fallback: %s", exc)
return _fallback(issues), False
markdown = (content or "").strip()
if not markdown:
logger.warning("Planning generation returned empty content, using fallback")
return _fallback(issues), False
logger.info("Generated AI planning for %d open tickets", len(issues))
return markdown[:PLAN_MAX], True
@@ -2,9 +2,11 @@
from __future__ import annotations
import asyncio
import hashlib
import logging
import re
import time
from dataclasses import dataclass, field
from typing import Awaitable, Callable
@@ -14,6 +16,7 @@ from devplacepy import stealth
from devplacepy.net_guard import BlockedAddressError, guard_public_url, guarded_async_client
from .pdf import MAX_PDF_BYTES, extract_pdf_text, is_pdf
from .phases import PHASE_CRAWLING
logger = logging.getLogger(__name__)
@@ -22,6 +25,7 @@ RSEARCH_TIMEOUT_SECONDS = 45.0
FETCH_TIMEOUT_SECONDS = 20.0
MAX_FETCH_BYTES = 2_500_000
RESULTS_PER_QUERY = 8
CRAWL_CONCURRENCY = 4
USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36 DevPlaceDeepSearchBot/1.0"
+33 -2
View File
@@ -5,10 +5,13 @@ from __future__ import annotations
import json
import logging
import re
from typing import Callable
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from .phases import PHASE_PLANNING
logger = logging.getLogger(__name__)
ENHANCE_TIMEOUT_SECONDS = 90.0
@@ -52,7 +55,20 @@ def _parse(text: str) -> list[str]:
return cleaned[:MAX_SUBQUERIES]
async def plan_queries(query: str, api_key: str) -> list[str]:
def _noop(frame: dict) -> None:
return None
async def plan_queries(
query: str, api_key: str, emit: Callable[[dict], None] = _noop
) -> list[str]:
emit(
{
"type": "substep",
"phase": PHASE_PLANNING,
"message": "Drafting diverse search angles",
}
)
payload = {
"model": INTERNAL_MODEL,
"messages": [
@@ -79,7 +95,22 @@ async def plan_queries(query: str, api_key: str) -> list[str]:
)
parsed = _parse(content)
if parsed:
emit(
{
"type": "substep",
"phase": PHASE_PLANNING,
"message": f"Planned {len(parsed)} search angles",
}
)
return parsed
except Exception as exc:
logger.warning("deepsearch query planner failed, using fallback: %s", exc)
return _fallback(query)
fallback = _fallback(query)
emit(
{
"type": "substep",
"phase": PHASE_PLANNING,
"message": f"Planner unavailable, using {len(fallback)} heuristic angles",
}
)
return fallback
@@ -0,0 +1,38 @@
# retoor <retoor@molodetz.nl>
from __future__ import annotations
PHASE_PLANNING = "planning"
PHASE_SEARCHING = "searching"
PHASE_CRAWLING = "crawling"
PHASE_INDEXING = "indexing"
PHASE_ANALYSIS = "analysis"
PHASE_SYNTHESIS = "synthesis"
PHASE_ORDER: list[str] = [
PHASE_PLANNING,
PHASE_SEARCHING,
PHASE_CRAWLING,
PHASE_INDEXING,
PHASE_ANALYSIS,
PHASE_SYNTHESIS,
]
PHASE_LABELS: dict[str, str] = {
PHASE_PLANNING: "Planning",
PHASE_SEARCHING: "Searching",
PHASE_CRAWLING: "Crawling",
PHASE_INDEXING: "Indexing",
PHASE_ANALYSIS: "Analysis",
PHASE_SYNTHESIS: "Synthesis",
}
TOTAL_PHASES = len(PHASE_ORDER)
HEARTBEAT_SECONDS = 2.0
def phase_index(phase: str) -> int:
try:
return PHASE_ORDER.index(phase)
except ValueError:
return 0
@@ -0,0 +1,119 @@
# retoor <retoor@molodetz.nl>
import logging
import zlib
from pathlib import Path
from devplacepy.attachments import _directory_for
from devplacepy.config import PLANNING_REPORTS_DIR
from devplacepy.services.gitea import runtime
from devplacepy.services.gitea.client import STATE_OPEN, GiteaError
from devplacepy.services.gitea.config import gitea_config
from devplacepy.services.gitea.planning import MAX_ISSUES, generate_plan
from devplacepy.services.jobs.base import JobService
from devplacepy.utils import slugify
logger = logging.getLogger(__name__)
PAGE_LIMIT = 50
class PlanningReportService(JobService):
kind = "planning"
title = "Ticket planning"
description = (
"Builds a grouped, ordered markdown planning report of all open Gitea tickets off "
"the request path using the internal AI service (with a deterministic fallback), "
"writes the markdown artifact, and prunes it once unused for the retention window."
)
def __init__(self) -> None:
super().__init__(name="planning", interval_seconds=2)
async def process(self, job: dict) -> dict:
from devplacepy.services.audit import record as audit
uid: str = job["uid"]
owner_kind: str = job.get("owner_kind") or "system"
owner_id: str = job.get("owner_id") or ""
try:
config = gitea_config()
if not config.is_configured:
raise GiteaError("Gitea integration is not configured", status=503)
issues = await self._collect_open(config)
markdown, ai_used = await generate_plan(issues, config)
issue_count = len(issues)
final_name = self._final_name(job.get("preferred_name", ""), markdown)
target_dir = PLANNING_REPORTS_DIR / _directory_for(uid)
target_dir.mkdir(parents=True, exist_ok=True)
final_path = target_dir / final_name
final_path.write_text(markdown, encoding="utf-8")
bytes_out = len(markdown.encode("utf-8"))
except Exception:
audit.record_system(
"issue.planning.generate",
actor_kind="user" if owner_kind == "user" else owner_kind,
actor_uid=owner_id if owner_kind == "user" else None,
result="failure",
target_type="issue",
metadata={"job": uid},
summary="planning report generation failed",
links=[audit.job(uid)],
)
raise
audit.record_system(
"issue.planning.generate",
actor_kind="user" if owner_kind == "user" else owner_kind,
actor_uid=owner_id if owner_kind == "user" else None,
target_type="issue",
metadata={
"issue_count": issue_count,
"ai_used": ai_used,
"bytes_out": bytes_out,
},
summary=f"planning report for {issue_count} open tickets generated",
links=[audit.job(uid)],
)
self.log(f"Planning job {uid} covered {issue_count} open tickets")
return {
"download_url": f"/issues/planning/{uid}/download",
"local_path": str(final_path),
"final_name": final_name,
"markdown": markdown,
"ai_used": ai_used,
"item_count": issue_count,
"bytes_out": bytes_out,
}
async def _collect_open(self, config) -> list[dict]:
client = runtime.get_client()
collected: list[dict] = []
page = 1
while len(collected) < MAX_ISSUES:
issues, total = await client.list_issues(
state=STATE_OPEN, page=page, limit=PAGE_LIMIT
)
if not issues:
break
collected.extend(issues)
if len(issues) < PAGE_LIMIT or len(collected) >= total:
break
page += 1
return collected[:MAX_ISSUES]
def _final_name(self, preferred_name: str, markdown: str) -> str:
name = preferred_name or "open-tickets-plan"
if name.lower().endswith(".md"):
name = name[:-3]
slug = slugify(name) or "open-tickets-plan"
crc32 = zlib.crc32(markdown.encode("utf-8")) & 0xFFFFFFFF
return f"{crc32:08x}.{slug}.md"
def cleanup(self, job: dict) -> None:
result = job.get("result", {})
local_path = result.get("local_path")
if local_path:
Path(local_path).unlink(missing_ok=True)