simplicio-cli 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {simplicio_cli-0.2.2/simplicio_cli.egg-info → simplicio_cli-0.2.3}/PKG-INFO +4 -4
  2. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/README.md +3 -3
  3. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/pyproject.toml +1 -1
  4. simplicio_cli-0.2.3/simplicio/bench.py +51 -0
  5. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio/cache.py +20 -20
  6. simplicio_cli-0.2.3/simplicio/cli.py +43 -0
  7. simplicio_cli-0.2.3/simplicio/pipeline.py +28 -0
  8. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio/precedent.py +24 -24
  9. simplicio_cli-0.2.3/simplicio/prompt.py +25 -0
  10. simplicio_cli-0.2.3/simplicio/providers.py +63 -0
  11. simplicio_cli-0.2.3/simplicio/skill_router.py +48 -0
  12. simplicio_cli-0.2.3/simplicio/templates/simplicio_prompt.md +43 -0
  13. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3/simplicio_cli.egg-info}/PKG-INFO +4 -4
  14. simplicio_cli-0.2.2/simplicio/bench.py +0 -51
  15. simplicio_cli-0.2.2/simplicio/cli.py +0 -43
  16. simplicio_cli-0.2.2/simplicio/pipeline.py +0 -28
  17. simplicio_cli-0.2.2/simplicio/prompt.py +0 -25
  18. simplicio_cli-0.2.2/simplicio/providers.py +0 -62
  19. simplicio_cli-0.2.2/simplicio/skill_router.py +0 -48
  20. simplicio_cli-0.2.2/simplicio/templates/simplicio_prompt.md +0 -43
  21. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/LICENSE +0 -0
  22. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/setup.cfg +0 -0
  23. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio/__init__.py +0 -0
  24. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio_cli.egg-info/SOURCES.txt +0 -0
  25. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio_cli.egg-info/dependency_links.txt +0 -0
  26. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio_cli.egg-info/entry_points.txt +0 -0
  27. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio_cli.egg-info/requires.txt +0 -0
  28. {simplicio_cli-0.2.2 → simplicio_cli-0.2.3}/simplicio_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Portable task-to-code pipeline that works with any LLM. Turn a one-line task into a verified code change — diff + test + verify loop. +55 pts on a 156-check benchmark, 21% faster, ~same tokens.
5
5
  Author-email: Wesley Simplicio <wesleybob4@gmail.com>
6
6
  License: MIT
@@ -147,10 +147,10 @@ simplicio index --stack angular
147
147
  # run a task
148
148
  simplicio task "hide Delete button for non-admins" \
149
149
  --stack angular \
150
- --alvo src/app/screen/screen.component.html \
151
- --criterios "- no admin perm: button absent from DOM
150
+ --target src/app/screen/screen.component.html \
151
+ --criteria "- no admin perm: button absent from DOM
152
152
  - with admin perm: button present" \
153
- --restricoes "- don't touch save flow
153
+ --constraints "- don't touch save flow
154
154
  - build passes"
155
155
  ```
156
156
 
@@ -112,10 +112,10 @@ simplicio index --stack angular
112
112
  # run a task
113
113
  simplicio task "hide Delete button for non-admins" \
114
114
  --stack angular \
115
- --alvo src/app/screen/screen.component.html \
116
- --criterios "- no admin perm: button absent from DOM
115
+ --target src/app/screen/screen.component.html \
116
+ --criteria "- no admin perm: button absent from DOM
117
117
  - with admin perm: button present" \
118
- --restricoes "- don't touch save flow
118
+ --constraints "- don't touch save flow
119
119
  - build passes"
120
120
  ```
121
121
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simplicio-cli"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Portable task-to-code pipeline that works with any LLM. Turn a one-line task into a verified code change — diff + test + verify loop. +55 pts on a 156-check benchmark, 21% faster, ~same tokens."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -0,0 +1,51 @@
1
+ """
2
+ bench.py — compares WITH vs WITHOUT the pipeline. REAL numbers, nothing made up.
3
+
4
+ Each case = {goal, target, criteria, test_cmd}.
5
+ WITHOUT: sends only the raw goal to the LLM (baseline).
6
+ WITH: full pipeline (precedent + skill + layers + verify).
7
+ Measures: did it pass the test on the first try? how many attempts?
8
+
9
+ Usage: simplicio bench --cases bench/cases.json --stack angular
10
+ Writes bench/results.md with the real table.
11
+ """
12
+ import os, json, time, subprocess
13
+ from .prompt import build_prompt
14
+ from .providers import generate
15
+
16
+ def _test(output, root, test_cmd):
17
+ os.makedirs(os.path.join(root, ".simplicio"), exist_ok=True)
18
+ open(os.path.join(root, ".simplicio/bench_out.txt"), "w").write(output or "")
19
+ p = subprocess.run(test_cmd, shell=True, cwd=root, capture_output=True, text=True)
20
+ return p.returncode == 0
21
+
22
+ def _baseline(case, root):
23
+ # baseline: raw goal, zero built context
24
+ return generate(case["goal"])
25
+
26
+ def _pipeline(case, root, stack):
27
+ return generate(build_prompt(root, stack, case["goal"], case["target"],
28
+ case.get("criteria","- true state\n- false state"),
29
+ case.get("constraints","- build passes")))
30
+
31
+ def run_bench(root, stack, cases_path):
32
+ cases = json.load(open(cases_path))
33
+ rows, n = [], len(cases)
34
+ hits = {"without": 0, "with": 0}
35
+ for c in cases:
36
+ tc = c["test_cmd"]
37
+ ok_without = _test(_baseline(c, root), root, tc); hits["without"] += ok_without
38
+ ok_with = _test(_pipeline(c, root, stack), root, tc); hits["with"] += ok_with
39
+ rows.append(f"| {c['goal'][:40]} | {'PASS' if ok_without else 'FAIL'} | {'PASS' if ok_with else 'FAIL'} |")
40
+ table = ("| Task | Without simplicio | With simplicio |\n|---|---|---|\n"
41
+ + "\n".join(rows)
42
+ + f"\n\n**First-try pass rate:** without = {hits['without']}/{n} "
43
+ f"({100*hits['without']//n}%) · with = {hits['with']}/{n} "
44
+ f"({100*hits['with']//n}%)")
45
+ out = os.path.join(root, "bench", "results.md")
46
+ os.makedirs(os.path.dirname(out), exist_ok=True)
47
+ open(out, "w").write(f"# Benchmark (generated by `simplicio bench`)\n\n"
48
+ f"Provider: {os.environ.get('SIMPLICIO_PROVIDER','claude')} · "
49
+ f"cases: {n} · date: {time.strftime('%Y-%m-%d')}\n\n{table}\n")
50
+ print(table)
51
+ print(f"\n-> {out}")
@@ -1,11 +1,11 @@
1
1
  """
2
- cache.py — cache de embeddings keyed por HASH do conteudo.
2
+ cache.py — embedding cache keyed by content HASH.
3
3
 
4
- Por que por hash e nao por arquivo: se o bloco de codigo nao mudou, o hash
5
- e o mesmo -> reusa o vetor. Arquivo muda mas o trecho relevante nao? Ainda
6
- acerta o cache. Trecho muda -> hash novo -> re-embedda SO ele. Granular.
4
+ Why hash, not file: if a code block didn't change, the hash is the same ->
5
+ reuse the vector. File changes but the relevant snippet didn't? Still a
6
+ cache hit. Snippet changes -> new hash -> only that one is re-embedded. Granular.
7
7
 
8
- Persistido em .simplicio/emb_cache.npz (vetores) + .json (indice hash->linha).
8
+ Persisted in .simplicio/emb_cache.npz (vectors) + .json (hash->row index).
9
9
  """
10
10
 
11
11
  import os, json, hashlib
@@ -17,13 +17,13 @@ class EmbeddingCache:
17
17
  os.makedirs(self.dir, exist_ok=True)
18
18
  self.vec_path = os.path.join(self.dir, "emb_cache.npz")
19
19
  self.idx_path = os.path.join(self.dir, "emb_index.json")
20
- self.index = {} # hash -> posicao na matriz
20
+ self.index = {} # hash -> row position in the matrix
21
21
  self.vectors = None # np.ndarray [N, dim]
22
22
  self._load()
23
23
 
24
24
  @staticmethod
25
- def h(texto):
26
- return hashlib.sha1(texto.encode("utf-8")).hexdigest()
25
+ def h(text):
26
+ return hashlib.sha1(text.encode("utf-8")).hexdigest()
27
27
 
28
28
  def _load(self):
29
29
  if os.path.exists(self.idx_path) and os.path.exists(self.vec_path):
@@ -35,23 +35,23 @@ class EmbeddingCache:
35
35
  if self.vectors is not None:
36
36
  np.savez_compressed(self.vec_path, v=self.vectors)
37
37
 
38
- def get_missing(self, textos):
39
- """Retorna os textos que NAO estao no cache (precisam embeddar)."""
40
- return [t for t in textos if self.h(t) not in self.index]
38
+ def get_missing(self, texts):
39
+ """Returns the texts NOT in the cache (need embedding)."""
40
+ return [t for t in texts if self.h(t) not in self.index]
41
41
 
42
- def add(self, textos, vetores):
43
- """Adiciona novos textos+vetores ao cache."""
44
- if not textos:
42
+ def add(self, texts, vectors):
43
+ """Adds new texts+vectors to the cache."""
44
+ if not texts:
45
45
  return
46
- vetores = np.asarray(vetores)
46
+ vectors = np.asarray(vectors)
47
47
  base = 0 if self.vectors is None else self.vectors.shape[0]
48
- self.vectors = vetores if self.vectors is None else np.vstack([self.vectors, vetores])
49
- for i, t in enumerate(textos):
48
+ self.vectors = vectors if self.vectors is None else np.vstack([self.vectors, vectors])
49
+ for i, t in enumerate(texts):
50
50
  self.index[self.h(t)] = base + i
51
51
 
52
- def lookup(self, textos):
53
- """Devolve matriz de vetores na ordem dos textos (todos ja no cache)."""
54
- rows = [self.index[self.h(t)] for t in textos]
52
+ def lookup(self, texts):
53
+ """Returns a matrix of vectors in the texts' order (all already cached)."""
54
+ rows = [self.index[self.h(t)] for t in texts]
55
55
  return self.vectors[rows]
56
56
 
57
57
  def stats(self):
@@ -0,0 +1,43 @@
1
+ """cli.py — commands: index (cache repo), task (run pipeline), bench, smoke."""
2
+ import argparse
3
+ from .precedent import index_repo
4
+ from .pipeline import run
5
+ from .bench import run_bench
6
+ from .providers import generate, info
7
+
8
+ def main():
9
+ ap = argparse.ArgumentParser(prog="simplicio")
10
+ sub = ap.add_subparsers(dest="cmd", required=True)
11
+
12
+ pi = sub.add_parser("index", help="index/cache the repo (once, or after changes)")
13
+ pi.add_argument("--root", default="."); pi.add_argument("--stack", default="angular")
14
+
15
+ pt = sub.add_parser("task", help="run a task")
16
+ pt.add_argument("goal")
17
+ pt.add_argument("--root", default="."); pt.add_argument("--stack", default="angular")
18
+ pt.add_argument("--target", required=True)
19
+ pt.add_argument("--criteria", default="- true state\n- false state")
20
+ pt.add_argument("--constraints", default="- build passes")
21
+
22
+
23
+ pb = sub.add_parser("bench", help="compare with vs without (real numbers)")
24
+ pb.add_argument("--root", default="."); pb.add_argument("--stack", default="angular")
25
+ pb.add_argument("--cases", default="bench/cases.json")
26
+
27
+
28
+ sub.add_parser("smoke", help="one proof call: connect+generate (needs SIMPLICIO_MODEL+KEY)")
29
+
30
+ a = ap.parse_args()
31
+ if a.cmd == "index":
32
+ index_repo(a.root, a.stack)
33
+ elif a.cmd == "smoke":
34
+ print("provider:", info())
35
+ out = generate("Reply exactly: OK simplicio connected.")
36
+ print("model reply:", out.strip()[:200])
37
+ elif a.cmd == "bench":
38
+ run_bench(a.root, a.stack, a.cases)
39
+ else:
40
+ run(a.root, a.stack, a.goal, a.target, a.criteria, a.constraints)
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -0,0 +1,28 @@
1
+ """pipeline.py — build -> generate -> apply -> test -> fix (loop)."""
2
+ import os, subprocess
3
+ from .prompt import build_prompt
4
+ from .providers import generate
5
+
6
+ MAX_ATTEMPTS = 3
7
+
8
+ def _apply_and_test(output, root):
9
+ os.makedirs(os.path.join(root, ".simplicio"), exist_ok=True)
10
+ open(os.path.join(root, ".simplicio/last_output.txt"), "w").write(output)
11
+ # PLUG: extract diff -> git apply; extract test. Here we run the test command.
12
+ cmd = os.environ.get("SIMPLICIO_TEST_CMD", "echo 'configure SIMPLICIO_TEST_CMD'")
13
+ p = subprocess.run(cmd, shell=True, cwd=root, capture_output=True, text=True)
14
+ return p.returncode == 0, (p.stdout + p.stderr)[-2000:]
15
+
16
+ def run(root, stack, goal, target, criteria, constraints):
17
+ prompt = build_prompt(root, stack, goal, target, criteria, constraints)
18
+ feedback = None
19
+ for t in range(1, MAX_ATTEMPTS + 1):
20
+ print(f"--- attempt {t} (provider={os.environ.get('SIMPLICIO_PROVIDER','claude')}) ---")
21
+ output = generate(prompt, feedback)
22
+ ok, log = _apply_and_test(output, root)
23
+ if ok:
24
+ print("PASSED the contract. DONE.")
25
+ return output
26
+ print("failed:", log[:300]); feedback = log
27
+ print("attempts exhausted — manual review needed.")
28
+ return None
@@ -1,5 +1,5 @@
1
1
  """
2
- precedent.py — acha PRECEDENTE usando o cache (so embedda bloco novo).
2
+ precedent.py — finds PRECEDENT using the cache (only embeds new blocks).
3
3
  """
4
4
  import re, glob, os
5
5
  import numpy as np
@@ -26,7 +26,7 @@ EXT = {"angular": (".html", ".ts"), "react": (".tsx", ".jsx", ".ts"),
26
26
  SKIP = ("node_modules", "/.git/", "/dist/", "/bin/", "/obj/", "/.angular/", "/.simplicio/")
27
27
 
28
28
 
29
- def grep_candidatos(root, stack, janela=1):
29
+ def grep_candidates(root, stack, window=1):
30
30
  pats = [re.compile(p) for p in PATTERNS[stack]]
31
31
  exts = EXT[stack]
32
32
  cands = []
@@ -34,39 +34,39 @@ def grep_candidatos(root, stack, janela=1):
34
34
  if not os.path.isfile(fp) or not fp.endswith(exts): continue
35
35
  if any(s in fp for s in SKIP): continue
36
36
  try:
37
- linhas = open(fp, encoding="utf-8", errors="ignore").read().splitlines()
37
+ lines = open(fp, encoding="utf-8", errors="ignore").read().splitlines()
38
38
  except Exception:
39
39
  continue
40
- for i, ln in enumerate(linhas):
40
+ for i, ln in enumerate(lines):
41
41
  if any(p.search(ln) for p in pats):
42
- bloco = "\n".join(linhas[max(0, i - janela): i + janela + 1])
43
- cands.append({"file": fp, "line": i + 1, "code": bloco})
42
+ block = "\n".join(lines[max(0, i - window): i + window + 1])
43
+ cands.append({"file": fp, "line": i + 1, "code": block})
44
44
  return cands
45
45
 
46
46
 
47
47
  def index_repo(root, stack, verbose=True):
48
- """Indexa: embedda SO os blocos novos, salva cache. Retorna stats."""
48
+ """Index: embed ONLY new blocks, persist cache. Returns stats."""
49
49
  cache = EmbeddingCache(root)
50
- cands = grep_candidatos(root, stack)
51
- textos = list({c["code"] for c in cands}) # dedup
52
- faltam = cache.get_missing(textos)
53
- if faltam:
54
- vetores = _embedder().encode(faltam, show_progress_bar=False)
55
- cache.add(faltam, vetores)
50
+ cands = grep_candidates(root, stack)
51
+ texts = list({c["code"] for c in cands}) # dedup
52
+ missing = cache.get_missing(texts)
53
+ if missing:
54
+ vectors = _embedder().encode(missing, show_progress_bar=False)
55
+ cache.add(missing, vectors)
56
56
  cache.save()
57
57
  if verbose:
58
- print(f"[index] candidatos={len(cands)} novos_embeddados={len(faltam)} "
58
+ print(f"[index] candidates={len(cands)} newly_embedded={len(missing)} "
59
59
  f"cache_total={cache.stats()['cached_blocks']}")
60
60
  return cache, cands
61
61
 
62
62
 
63
- def montar_bloco_precedente(root, stack, tarefa, k=2):
63
+ def build_precedent_block(root, stack, task, k=2):
64
64
  cache, cands = index_repo(root, stack, verbose=False)
65
65
  if not cands:
66
- return "[PRECEDENTE]\n(nenhum padrao similar no repo — gere do zero pela convencao da stack)"
67
- textos = [c["code"] for c in cands]
68
- vc = cache.lookup(textos) # do cache, sem re-embeddar
69
- vt = _embedder().encode([tarefa])[0] # so a tarefa (curta)
66
+ return "[PRECEDENT]\n(no similar pattern in repo — generate from scratch using stack convention)"
67
+ texts = [c["code"] for c in cands]
68
+ vc = cache.lookup(texts) # from cache, no re-embed
69
+ vt = _embedder().encode([task])[0] # only the task (short)
70
70
  for c, v in zip(cands, vc):
71
71
  c["score"] = float(np.dot(vt, v) / (np.linalg.norm(vt) * np.linalg.norm(v)))
72
72
  seen, out = set(), []
@@ -74,10 +74,10 @@ def montar_bloco_precedente(root, stack, tarefa, k=2):
74
74
  if c["code"] in seen: continue
75
75
  seen.add(c["code"]); out.append(c)
76
76
  tops = out[:k]
77
- linhas = ["[PRECEDENTE]",
78
- "Este projeto JA faz algo parecido. Siga ESTA convencao, nao invente:"]
77
+ lines = ["[PRECEDENT]",
78
+ "This project ALREADY does something similar. Follow THIS convention, don't invent:"]
79
79
  for c in tops:
80
80
  rel = os.path.relpath(c["file"], root)
81
- linhas.append(f"\n# {rel}:{c['line']} (similaridade {c['score']:.2f})")
82
- linhas.append(c["code"])
83
- return "\n".join(linhas)
81
+ lines.append(f"\n# {rel}:{c['line']} (similarity {c['score']:.2f})")
82
+ lines.append(c["code"])
83
+ return "\n".join(lines)
@@ -0,0 +1,25 @@
1
+ """prompt.py — stacks the 6 layers."""
2
+ import os, re
3
+ from .precedent import build_precedent_block
4
+ from .skill_router import build_skill_block
5
+
6
+ def _mapper(root, target):
7
+ try:
8
+ txt = open(os.path.join(root, target), encoding="utf-8", errors="ignore").read()
9
+ except Exception:
10
+ return "(mapper: target not read)"
11
+ deps = [l for l in txt.splitlines()
12
+ if l.strip().startswith(("import", "using", "from"))][:15]
13
+ return f"File: {target}\nDependencies:\n" + "\n".join(deps)
14
+
15
+ def build_prompt(root, stack, goal, target, criteria, constraints):
16
+ tpl_path = os.path.join(os.path.dirname(__file__), "templates", "simplicio_prompt.md")
17
+ tpl = open(tpl_path, encoding="utf-8").read()
18
+ prec = build_precedent_block(root, stack, goal, k=2)
19
+ skill = build_skill_block(root, goal)
20
+ target_block = f"{target}\n\nTarget context:\n{_mapper(root, target)}"
21
+ for s, v in {"{{STACK}}": stack, "{{GOAL}}": goal,
22
+ "{{TARGET}}": target_block, "{{PRECEDENT}}": prec, "{{SKILL}}": skill,
23
+ "{{CRITERIA}}": criteria, "{{CONSTRAINTS}}": constraints}.items():
24
+ tpl = tpl.replace(s, v)
25
+ return re.sub(r"\{#.*?#\}", "", tpl, flags=re.DOTALL).strip()
@@ -0,0 +1,63 @@
1
+ """
2
+ providers.py — provider-agnostic. Does NOT list specific models.
3
+
4
+ Configure via env (or --model/--base flags):
5
+ SIMPLICIO_MODEL model id, exactly as the provider expects
6
+ e.g. "anthropic/claude-opus-4", "openai/gpt-4.1",
7
+ "z-ai/glm-4.6", "deepseek/deepseek-chat", "any/thing"
8
+ SIMPLICIO_BASE_URL OpenAI-compatible endpoint
9
+ OpenRouter: https://openrouter.ai/api/v1
10
+ GLM: https://api.z.ai/api/paas/v4
11
+ local: http://localhost:11434/v1
12
+ SIMPLICIO_API_KEY the key (any provider)
13
+
14
+ Native Anthropic path (no base_url): if SIMPLICIO_BASE_URL is empty AND the
15
+ key is ANTHROPIC_API_KEY, the anthropic SDK is used. Otherwise an
16
+ OpenAI-compatible client is used pointing at base_url — works for ANY
17
+ OpenAI-like provider.
18
+ """
19
+ import os
20
+
21
+ def _cfg():
22
+ return {
23
+ "model": os.environ.get("SIMPLICIO_MODEL"),
24
+ "base": os.environ.get("SIMPLICIO_BASE_URL"),
25
+ "key": os.environ.get("SIMPLICIO_API_KEY")
26
+ or os.environ.get("OPENROUTER_API_KEY")
27
+ or os.environ.get("ANTHROPIC_API_KEY"),
28
+ }
29
+
30
+ def _msgs(prompt, feedback):
31
+ m = [{"role": "user", "content": prompt}]
32
+ if feedback:
33
+ m.append({"role": "user",
34
+ "content": f"The test FAILED:\n{feedback}\nFix it. Same output format."})
35
+ return m
36
+
37
+ def generate(prompt, feedback=None, max_tokens=4000):
38
+ c = _cfg()
39
+ if not c["model"]:
40
+ raise SystemExit("set SIMPLICIO_MODEL (model id for your provider)")
41
+ if not c["key"]:
42
+ raise SystemExit("set SIMPLICIO_API_KEY (or OPENROUTER_/ANTHROPIC_API_KEY)")
43
+
44
+ # native Anthropic path: no base_url
45
+ if not c["base"]:
46
+ import anthropic
47
+ cli = anthropic.Anthropic(api_key=c["key"])
48
+ r = cli.messages.create(model=c["model"], max_tokens=max_tokens,
49
+ messages=_msgs(prompt, feedback))
50
+ return next((b.text for b in r.content if b.type == "text"), "")
51
+
52
+ # any OpenAI-compatible endpoint (OpenRouter, GLM, DeepSeek, local...)
53
+ from openai import OpenAI
54
+ cli = OpenAI(base_url=c["base"], api_key=c["key"])
55
+ r = cli.chat.completions.create(model=c["model"], max_tokens=max_tokens,
56
+ messages=_msgs(prompt, feedback))
57
+ return r.choices[0].message.content
58
+
59
+ def info():
60
+ c = _cfg()
61
+ return (f"model={c['model'] or '(unset)'} "
62
+ f"base={c['base'] or 'anthropic-native'} "
63
+ f"key={'set' if c['key'] else 'MISSING'}")
@@ -0,0 +1,48 @@
1
+ """
2
+ skill_router.py — LAYER 4.5. Picks THE skill that matches the task.
3
+ Does NOT inject all of them (noise). Ranks by meaning, takes top-1.
4
+
5
+ Skill source: by default scans <root>/.mapper/skills/*.md or
6
+ SIMPLICIO_SKILLS_DIR. Each skill = md file whose first line is the description.
7
+ Reuses the same embedding cache.
8
+ """
9
+ import os, glob
10
+ import numpy as np
11
+ from .cache import EmbeddingCache
12
+
13
+ def _skills_dir(root):
14
+ return os.environ.get("SIMPLICIO_SKILLS_DIR",
15
+ os.path.join(root, ".mapper", "skills"))
16
+
17
+ def _load_skills(root):
18
+ d = _skills_dir(root)
19
+ out = []
20
+ for fp in glob.glob(os.path.join(d, "*.md")):
21
+ try:
22
+ txt = open(fp, encoding="utf-8", errors="ignore").read()
23
+ except Exception:
24
+ continue
25
+ desc = next((l.strip("# ").strip() for l in txt.splitlines() if l.strip()), "")
26
+ out.append({"name": os.path.basename(fp), "desc": desc, "body": txt})
27
+ return out
28
+
29
+ def build_skill_block(root, task, threshold=0.15):
30
+ skills = _load_skills(root)
31
+ if not skills:
32
+ return "" # no skills -> layer disappears, no noise
33
+ from .precedent import _embedder
34
+ cache = EmbeddingCache(root)
35
+ descs = [s["desc"] for s in skills]
36
+ missing = cache.get_missing(descs)
37
+ if missing:
38
+ cache.add(missing, _embedder().encode(missing, show_progress_bar=False))
39
+ cache.save()
40
+ vd = cache.lookup(descs)
41
+ vt = _embedder().encode([task])[0]
42
+ scores = [float(np.dot(vt, v)/(np.linalg.norm(vt)*np.linalg.norm(v))) for v in vd]
43
+ i = int(np.argmax(scores))
44
+ if scores[i] < threshold:
45
+ return "" # nothing matches enough -> don't force an irrelevant skill
46
+ s = skills[i]
47
+ return (f"[RELEVANT SKILL]\nThe mapper has a method that matches this task "
48
+ f"(match {scores[i]:.2f}). Follow it:\n# {s['name']}\n{s['body'][:1200]}")
@@ -0,0 +1,43 @@
1
+ {# ============================================================
2
+ SIMPLICIO-PROMPT — 6 layers. Order: fixed (bottom/cache-friendly)
3
+ -> variable (top). Filled in by run_task.py.
4
+ {{...}} = slots the toolchain injects automatically.
5
+ ============================================================ #}
6
+
7
+ {# ---------- LAYER 1: ROLE + STACK (fixed, cached) ---------- #}
8
+ You are a senior engineer working IN THIS project.
9
+ Stack: {{STACK}}.
10
+ Project conventions are LAW. Do not bring generic patterns from the internet.
11
+ Do not invent files, libraries, or abstractions the project does not use.
12
+
13
+ {# ---------- LAYER 2: GOAL (1 line, zero ambiguity) ---------- #}
14
+ [GOAL]
15
+ {{GOAL}}
16
+
17
+ {# ---------- LAYER 3: TARGET (only the files you may touch) ---------- #}
18
+ [TARGET]
19
+ Touch ONLY these files:
20
+ {{TARGET}}
21
+
22
+ {# ---------- LAYER 4: PRECEDENT (the gold — from precedent.py) ---------- #}
23
+ {{PRECEDENT}}
24
+
25
+ {{SKILL}}
26
+
27
+ {# ---------- LAYER 5: CONTRACT (testable states + what not to break) ---------- #}
28
+ [CONTRACT]
29
+ Done WHEN, and only when, ALL of the states below are true:
30
+ {{CRITERIA}}
31
+
32
+ Constraints (do not break):
33
+ {{CONSTRAINTS}}
34
+
35
+ {# ---------- LAYER 6: OUTPUT (exact shape) ---------- #}
36
+ [OUTPUT]
37
+ Return EXACTLY in this shape, nothing else:
38
+ 1. Unified DIFF, target files only.
39
+ 2. TEST: test code that verifies each state of the [CONTRACT]
40
+ (one case per criterion — true AND false state).
41
+ 3. EVIDENCE: Playwright script that captures screenshots of the UI states,
42
+ if the task is visual. Otherwise, write "N/A".
43
+ No prose, no preamble.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Portable task-to-code pipeline that works with any LLM. Turn a one-line task into a verified code change — diff + test + verify loop. +55 pts on a 156-check benchmark, 21% faster, ~same tokens.
5
5
  Author-email: Wesley Simplicio <wesleybob4@gmail.com>
6
6
  License: MIT
@@ -147,10 +147,10 @@ simplicio index --stack angular
147
147
  # run a task
148
148
  simplicio task "hide Delete button for non-admins" \
149
149
  --stack angular \
150
- --alvo src/app/screen/screen.component.html \
151
- --criterios "- no admin perm: button absent from DOM
150
+ --target src/app/screen/screen.component.html \
151
+ --criteria "- no admin perm: button absent from DOM
152
152
  - with admin perm: button present" \
153
- --restricoes "- don't touch save flow
153
+ --constraints "- don't touch save flow
154
154
  - build passes"
155
155
  ```
156
156
 
@@ -1,51 +0,0 @@
1
- """
2
- bench.py — compara COM vs SEM o pipeline. Numeros REAIS, nada inventado.
3
-
4
- Cada caso = {objetivo, alvo, criterios, test_cmd}.
5
- SEM: manda so o objetivo cru pro LLM (baseline).
6
- COM: pipeline completo (precedent + skill + camadas + verify).
7
- Mede: passou no teste de primeira? quantas tentativas?
8
-
9
- Uso: simplicio bench --cases bench/cases.json --stack angular
10
- Preenche bench/results.md com a tabela real.
11
- """
12
- import os, json, time, subprocess
13
- from .prompt import montar
14
- from .providers import gerar
15
-
16
- def _testa(saida, root, test_cmd):
17
- os.makedirs(os.path.join(root, ".simplicio"), exist_ok=True)
18
- open(os.path.join(root, ".simplicio/bench_out.txt"), "w").write(saida or "")
19
- p = subprocess.run(test_cmd, shell=True, cwd=root, capture_output=True, text=True)
20
- return p.returncode == 0
21
-
22
- def _sem(caso, root):
23
- # baseline: objetivo cru, zero contexto montado
24
- return gerar(caso["objetivo"])
25
-
26
- def _com(caso, root, stack):
27
- return gerar(montar(root, stack, caso["objetivo"], caso["alvo"],
28
- caso.get("criterios","- estado verdadeiro\n- estado falso"),
29
- caso.get("restricoes","- build passa")))
30
-
31
- def run_bench(root, stack, cases_path):
32
- casos = json.load(open(cases_path))
33
- linhas, n = [], len(casos)
34
- acerto = {"sem": 0, "com": 0}
35
- for c in casos:
36
- tc = c["test_cmd"]
37
- ok_sem = _testa(_sem(c, root), root, tc); acerto["sem"] += ok_sem
38
- ok_com = _testa(_com(c, root, stack), root, tc); acerto["com"] += ok_com
39
- linhas.append(f"| {c['objetivo'][:40]} | {'✅' if ok_sem else '❌'} | {'✅' if ok_com else '❌'} |")
40
- tabela = ("| Tarefa | Sem simplicio | Com simplicio |\n|---|---|---|\n"
41
- + "\n".join(linhas)
42
- + f"\n\n**Acerto de primeira:** sem = {acerto['sem']}/{n} "
43
- f"({100*acerto['sem']//n}%) · com = {acerto['com']}/{n} "
44
- f"({100*acerto['com']//n}%)")
45
- out = os.path.join(root, "bench", "results.md")
46
- os.makedirs(os.path.dirname(out), exist_ok=True)
47
- open(out, "w").write(f"# Benchmark (gerado por `simplicio bench`)\n\n"
48
- f"Provider: {os.environ.get('SIMPLICIO_PROVIDER','claude')} · "
49
- f"casos: {n} · data: {time.strftime('%Y-%m-%d')}\n\n{tabela}\n")
50
- print(tabela)
51
- print(f"\n-> {out}")
@@ -1,43 +0,0 @@
1
- """cli.py — comandos: index (cacheia repo) e task (roda o pipeline)."""
2
- import argparse
3
- from .precedent import index_repo
4
- from .pipeline import run
5
- from .bench import run_bench
6
- from .providers import gerar, info
7
-
8
- def main():
9
- ap = argparse.ArgumentParser(prog="simplicio")
10
- sub = ap.add_subparsers(dest="cmd", required=True)
11
-
12
- pi = sub.add_parser("index", help="indexa/cacheia o repo (1x ou apos mudancas)")
13
- pi.add_argument("--root", default="."); pi.add_argument("--stack", default="angular")
14
-
15
- pt = sub.add_parser("task", help="executa uma tarefa")
16
- pt.add_argument("objetivo")
17
- pt.add_argument("--root", default="."); pt.add_argument("--stack", default="angular")
18
- pt.add_argument("--alvo", required=True)
19
- pt.add_argument("--criterios", default="- estado verdadeiro\n- estado falso")
20
- pt.add_argument("--restricoes", default="- build passa")
21
-
22
-
23
- pb = sub.add_parser("bench", help="compara com vs sem (numeros reais)")
24
- pb.add_argument("--root", default="."); pb.add_argument("--stack", default="angular")
25
- pb.add_argument("--cases", default="bench/cases.json")
26
-
27
-
28
- sub.add_parser("smoke", help="1 chamada de prova: conecta+gera (precisa SIMPLICIO_MODEL+KEY)")
29
-
30
- a = ap.parse_args()
31
- if a.cmd == "index":
32
- index_repo(a.root, a.stack)
33
- elif a.cmd == "smoke":
34
- print("provider:", info())
35
- out = gerar("Responda exatamente: OK simplicio conectado.")
36
- print("resposta do modelo:", out.strip()[:200])
37
- elif a.cmd == "bench":
38
- run_bench(a.root, a.stack, a.cases)
39
- else:
40
- run(a.root, a.stack, a.objetivo, a.alvo, a.criterios, a.restricoes)
41
-
42
- if __name__ == "__main__":
43
- main()
@@ -1,28 +0,0 @@
1
- """pipeline.py — monta -> gera -> aplica -> testa -> corrige (loop)."""
2
- import os, subprocess
3
- from .prompt import montar
4
- from .providers import gerar
5
-
6
- MAX_TENTATIVAS = 3
7
-
8
- def _aplicar_e_testar(saida, root):
9
- os.makedirs(os.path.join(root, ".simplicio"), exist_ok=True)
10
- open(os.path.join(root, ".simplicio/ultima_saida.txt"), "w").write(saida)
11
- # PLUGUE: extrair diff -> git apply; extrair teste. Aqui roda o cmd de teste.
12
- cmd = os.environ.get("SIMPLICIO_TEST_CMD", "echo 'configure SIMPLICIO_TEST_CMD'")
13
- p = subprocess.run(cmd, shell=True, cwd=root, capture_output=True, text=True)
14
- return p.returncode == 0, (p.stdout + p.stderr)[-2000:]
15
-
16
- def run(root, stack, objetivo, alvo, criterios, restricoes):
17
- prompt = montar(root, stack, objetivo, alvo, criterios, restricoes)
18
- feedback = None
19
- for t in range(1, MAX_TENTATIVAS + 1):
20
- print(f"--- tentativa {t} (provider={os.environ.get('SIMPLICIO_PROVIDER','claude')}) ---")
21
- saida = gerar(prompt, feedback)
22
- ok, log = _aplicar_e_testar(saida, root)
23
- if ok:
24
- print("PASSOU no contrato. PRONTO.")
25
- return saida
26
- print("falhou:", log[:300]); feedback = log
27
- print("esgotou tentativas — revisar manual.")
28
- return None
@@ -1,25 +0,0 @@
1
- """prompt.py — empilha as 6 camadas."""
2
- import os, re
3
- from .precedent import montar_bloco_precedente
4
- from .skill_router import montar_bloco_skill
5
-
6
- def _mapper(root, alvo):
7
- try:
8
- txt = open(os.path.join(root, alvo), encoding="utf-8", errors="ignore").read()
9
- except Exception:
10
- return "(mapper: alvo nao lido)"
11
- deps = [l for l in txt.splitlines()
12
- if l.strip().startswith(("import", "using", "from"))][:15]
13
- return f"Arquivo: {alvo}\nDependencias:\n" + "\n".join(deps)
14
-
15
- def montar(root, stack, objetivo, alvo, criterios, restricoes):
16
- tpl_path = os.path.join(os.path.dirname(__file__), "templates", "simplicio_prompt.md")
17
- tpl = open(tpl_path, encoding="utf-8").read()
18
- prec = montar_bloco_precedente(root, stack, objetivo, k=2)
19
- skill = montar_bloco_skill(root, objetivo)
20
- alvo_bloco = f"{alvo}\n\nContexto do alvo:\n{_mapper(root, alvo)}"
21
- for s, v in {"{{STACK}}": stack, "{{OBJETIVO}}": objetivo,
22
- "{{ALVO}}": alvo_bloco, "{{PRECEDENTE}}": prec, "{{SKILL}}": skill,
23
- "{{CRITERIOS}}": criterios, "{{RESTRICOES}}": restricoes}.items():
24
- tpl = tpl.replace(s, v)
25
- return re.sub(r"\{#.*?#\}", "", tpl, flags=re.DOTALL).strip()
@@ -1,62 +0,0 @@
1
- """
2
- providers.py — agnostico de provider. NAO lista modelos especificos.
3
-
4
- Voce define por env (ou flag --model/--base):
5
- SIMPLICIO_MODEL id do modelo, exatamente como o provider espera
6
- ex: "anthropic/claude-opus-4", "openai/gpt-4.1",
7
- "z-ai/glm-4.6", "deepseek/deepseek-chat", "qualquer/coisa"
8
- SIMPLICIO_BASE_URL endpoint OpenAI-compativel
9
- ex OpenRouter: https://openrouter.ai/api/v1
10
- ex GLM: https://api.z.ai/api/paas/v4
11
- ex local: http://localhost:11434/v1
12
- SIMPLICIO_API_KEY a chave (qualquer provider)
13
-
14
- Caminho nativo Anthropic (sem base_url): se SIMPLICIO_BASE_URL estiver vazio
15
- E a key for ANTHROPIC_API_KEY, usa o SDK anthropic. Senao, usa cliente
16
- OpenAI-compativel apontando pro base_url -> serve QUALQUER provider OAI-like.
17
- """
18
- import os
19
-
20
- def _cfg():
21
- return {
22
- "model": os.environ.get("SIMPLICIO_MODEL"),
23
- "base": os.environ.get("SIMPLICIO_BASE_URL"),
24
- "key": os.environ.get("SIMPLICIO_API_KEY")
25
- or os.environ.get("OPENROUTER_API_KEY")
26
- or os.environ.get("ANTHROPIC_API_KEY"),
27
- }
28
-
29
- def _msgs(prompt, feedback):
30
- m = [{"role": "user", "content": prompt}]
31
- if feedback:
32
- m.append({"role": "user",
33
- "content": f"O teste FALHOU:\n{feedback}\nCorrija. Mesmo formato."})
34
- return m
35
-
36
- def gerar(prompt, feedback=None, max_tokens=4000):
37
- c = _cfg()
38
- if not c["model"]:
39
- raise SystemExit("defina SIMPLICIO_MODEL (id do modelo do seu provider)")
40
- if not c["key"]:
41
- raise SystemExit("defina SIMPLICIO_API_KEY (ou OPENROUTER_/ANTHROPIC_API_KEY)")
42
-
43
- # caminho nativo Anthropic: sem base_url
44
- if not c["base"]:
45
- import anthropic
46
- cli = anthropic.Anthropic(api_key=c["key"])
47
- r = cli.messages.create(model=c["model"], max_tokens=max_tokens,
48
- messages=_msgs(prompt, feedback))
49
- return next((b.text for b in r.content if b.type == "text"), "")
50
-
51
- # qualquer endpoint OpenAI-compativel (OpenRouter, GLM, DeepSeek, local...)
52
- from openai import OpenAI
53
- cli = OpenAI(base_url=c["base"], api_key=c["key"])
54
- r = cli.chat.completions.create(model=c["model"], max_tokens=max_tokens,
55
- messages=_msgs(prompt, feedback))
56
- return r.choices[0].message.content
57
-
58
- def info():
59
- c = _cfg()
60
- return (f"model={c['model'] or '(nao definido)'} "
61
- f"base={c['base'] or 'anthropic-nativo'} "
62
- f"key={'set' if c['key'] else 'FALTA'}")
@@ -1,48 +0,0 @@
1
- """
2
- skill_router.py — CAMADA 4.5. Seleciona A skill que casa com a tarefa.
3
- NAO injeta todas (ruido). Ranqueia por sentido, pega top-1.
4
-
5
- Fonte das skills: por padrao varre <root>/.mapper/skills/*.md ou
6
- SIMPLICIO_SKILLS_DIR. Cada skill = arquivo md com 1a linha = descricao.
7
- Reusa o mesmo cache de embeddings.
8
- """
9
- import os, glob
10
- import numpy as np
11
- from .cache import EmbeddingCache
12
-
13
- def _skills_dir(root):
14
- return os.environ.get("SIMPLICIO_SKILLS_DIR",
15
- os.path.join(root, ".mapper", "skills"))
16
-
17
- def _carregar_skills(root):
18
- d = _skills_dir(root)
19
- out = []
20
- for fp in glob.glob(os.path.join(d, "*.md")):
21
- try:
22
- txt = open(fp, encoding="utf-8", errors="ignore").read()
23
- except Exception:
24
- continue
25
- desc = next((l.strip("# ").strip() for l in txt.splitlines() if l.strip()), "")
26
- out.append({"nome": os.path.basename(fp), "desc": desc, "corpo": txt})
27
- return out
28
-
29
- def montar_bloco_skill(root, tarefa, limiar=0.15):
30
- skills = _carregar_skills(root)
31
- if not skills:
32
- return "" # sem skills -> camada some, sem ruido
33
- from .precedent import _embedder
34
- cache = EmbeddingCache(root)
35
- descs = [s["desc"] for s in skills]
36
- faltam = cache.get_missing(descs)
37
- if faltam:
38
- cache.add(faltam, _embedder().encode(faltam, show_progress_bar=False))
39
- cache.save()
40
- vd = cache.lookup(descs)
41
- vt = _embedder().encode([tarefa])[0]
42
- scores = [float(np.dot(vt, v)/(np.linalg.norm(vt)*np.linalg.norm(v))) for v in vd]
43
- i = int(np.argmax(scores))
44
- if scores[i] < limiar:
45
- return "" # nada casa o suficiente -> nao força skill irrelevante
46
- s = skills[i]
47
- return (f"[SKILL RELEVANTE]\nO mapper tem um metodo que casa com esta tarefa "
48
- f"(match {scores[i]:.2f}). Siga-o:\n# {s['nome']}\n{s['corpo'][:1200]}")
@@ -1,43 +0,0 @@
1
- {# ============================================================
2
- SIMPLICIO-PROMPT — 6 camadas. Ordem: fixo (embaixo/cacheável)
3
- -> variável (em cima). Preenchido por run_task.py.
4
- {{...}} = slots que a toolchain injeta automaticamente.
5
- ============================================================ #}
6
-
7
- {# ---------- CAMADA 1: PAPEL + STACK (fixo, cacheia) ---------- #}
8
- Voce e um engenheiro senior trabalhando NESTE projeto.
9
- Stack: {{STACK}}.
10
- Convencoes deste projeto sao LEI. Nao traga padrao generico da internet.
11
- Nao invente arquivo, lib ou abstracao que o projeto nao usa.
12
-
13
- {# ---------- CAMADA 2: OBJETIVO (1 linha, zero ambiguidade) ---------- #}
14
- [OBJETIVO]
15
- {{OBJETIVO}}
16
-
17
- {# ---------- CAMADA 3: ALVO (so os arquivos que se toca) ---------- #}
18
- [ALVO]
19
- Toque SOMENTE nestes arquivos:
20
- {{ALVO}}
21
-
22
- {# ---------- CAMADA 4: PRECEDENTE (o ouro — vem do precedent.py) ---------- #}
23
- {{PRECEDENTE}}
24
-
25
- {{SKILL}}
26
-
27
- {# ---------- CAMADA 5: CONTRATO (estados testaveis + o que nao quebrar) ---------- #}
28
- [CONTRATO]
29
- Pronto QUANDO, e somente quando, TODOS os estados abaixo forem verdade:
30
- {{CRITERIOS}}
31
-
32
- Restricoes (nao quebrar):
33
- {{RESTRICOES}}
34
-
35
- {# ---------- CAMADA 6: SAIDA (formato exato) ---------- #}
36
- [SAIDA]
37
- Devolva EXATAMENTE neste formato, nada mais:
38
- 1. DIFF unificado, so dos arquivos do [ALVO].
39
- 2. TESTE: codigo de teste que verifica cada estado do [CONTRATO]
40
- (um caso por criterio — estado verdadeiro E falso).
41
- 3. EVIDENCIA: script Playwright que captura print dos estados de UI,
42
- se a tarefa for visual. Senao, escreva "N/A".
43
- Sem explicacao, sem preambulo.
File without changes
File without changes