pythia-plsql 0.4.10__tar.gz → 0.5.0__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 (41) hide show
  1. {pythia_plsql-0.4.10/scripts/pythia_plsql.egg-info → pythia_plsql-0.5.0}/PKG-INFO +7 -7
  2. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/README.md +6 -6
  3. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/pyproject.toml +1 -1
  4. pythia_plsql-0.5.0/queries/object-names.sql +11 -0
  5. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/scripts/pythia.py +181 -30
  6. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0/scripts/pythia_plsql.egg-info}/PKG-INFO +7 -7
  7. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/scripts/pythia_plsql.egg-info/SOURCES.txt +2 -0
  8. pythia_plsql-0.5.0/skills/pythia-conventions/SKILL.md +101 -0
  9. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/tests/test_phase2.py +64 -0
  10. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/tests/test_phase5.py +1 -1
  11. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/LICENSE +0 -0
  12. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/compile-errors.sql +0 -0
  13. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/dependencies.sql +0 -0
  14. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/impact.sql +0 -0
  15. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/invalid-objects.sql +0 -0
  16. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/name-occupants.sql +0 -0
  17. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/object-source.sql +0 -0
  18. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/plscope-enabled.sql +0 -0
  19. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/plscope-statements.sql +0 -0
  20. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/plscope-usages.sql +0 -0
  21. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/session-privileges.sql +0 -0
  22. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/similar-candidates.sql +0 -0
  23. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/queries/source.sql +0 -0
  24. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  25. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  26. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  27. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  28. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/setup.cfg +0 -0
  29. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-apply/SKILL.md +0 -0
  30. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-explore/SKILL.md +0 -0
  31. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-explore/reference/data-dictionary.md +0 -0
  32. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-impact/SKILL.md +0 -0
  33. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-review/SKILL.md +0 -0
  34. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-review/reference/antipatterns.md +0 -0
  35. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-setup/SKILL.md +0 -0
  36. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-skill-author/SKILL.md +0 -0
  37. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-write/SKILL.md +0 -0
  38. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/skills/pythia-write/reference/patterns.md +0 -0
  39. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/tests/test_install.py +0 -0
  40. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/tests/test_phase1.py +0 -0
  41. {pythia_plsql-0.4.10 → pythia_plsql-0.5.0}/tests/test_phase3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.4.10
3
+ Version: 0.5.0
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -178,13 +178,13 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
178
178
 
179
179
  ## Skills
180
180
 
181
- Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
181
+ Eight skills teach the agent the workflow — gates, not suggestions:
182
182
 
183
- `pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
184
- `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
185
- developer sees the preview and approves in chat before anything is written) ·
186
- `pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
187
- workflow as a new skill, mined from the live schema).
183
+ `pythia-setup` · `pythia-explore` · `pythia-impact` (before any change) ·
184
+ `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate:
185
+ the developer approves the preview in chat) · `pythia-review` (antipatterns) ·
186
+ `pythia-conventions` (adopt a house style, verified against real names) ·
187
+ `pythia-skill-author` (capture your team's workflow as a skill).
188
188
 
189
189
  ## Compatibility
190
190
 
@@ -164,13 +164,13 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
164
164
 
165
165
  ## Skills
166
166
 
167
- Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
167
+ Eight skills teach the agent the workflow — gates, not suggestions:
168
168
 
169
- `pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
170
- `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
171
- developer sees the preview and approves in chat before anything is written) ·
172
- `pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
173
- workflow as a new skill, mined from the live schema).
169
+ `pythia-setup` · `pythia-explore` · `pythia-impact` (before any change) ·
170
+ `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate:
171
+ the developer approves the preview in chat) · `pythia-review` (antipatterns) ·
172
+ `pythia-conventions` (adopt a house style, verified against real names) ·
173
+ `pythia-skill-author` (capture your team's workflow as a skill).
174
174
 
175
175
  ## Compatibility
176
176
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythia-plsql"
7
- version = "0.4.10"
7
+ version = "0.5.0"
8
8
  description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,11 @@
1
+ -- Purpose: every object name in the schema with its type, for measuring how
2
+ -- well a naming pattern describes what is actually there. Derived
3
+ -- patterns are guesses until the schema agrees with them.
4
+ -- Binds: :s schema (object owner)
5
+ -- Returns: OBJECT_TYPE, OBJECT_NAME
6
+ select object_type,
7
+ object_name
8
+ from all_objects
9
+ where owner = :s
10
+ and object_type not in ('INDEX', 'LOB', 'TABLE PARTITION', 'INDEX PARTITION')
11
+ order by object_type, object_name
@@ -85,6 +85,7 @@ QUERY_BINDS = {
85
85
  "object-source.sql": {"s", "n", "t"},
86
86
  "session-privileges.sql": set(),
87
87
  "name-occupants.sql": {"s", "n"},
88
+ "object-names.sql": {"s"},
88
89
  }
89
90
 
90
91
 
@@ -1332,6 +1333,24 @@ def cmd_plscope(conn, schema, ns):
1332
1333
  emit_table(ns, stmt_cols, *clip(stmt_rows, ns.limit))
1333
1334
 
1334
1335
 
1336
+ AGENT_USER_SQL = """\
1337
+ -- Least-privilege agent credential for schema {owner} — run as a DBA.
1338
+ -- Proxy authentication: the agent logs in with its OWN password but works
1339
+ -- inside {owner}; it never learns the owner password, owns nothing, and
1340
+ -- revocation is one statement. Deliberately absent: DBA, RESOURCE, ANY
1341
+ -- privileges, utility grants — the agent needs none of them to develop
1342
+ -- PL/SQL, and every extra grant widens the blast radius.
1343
+
1344
+ {create_line}
1345
+ GRANT CREATE SESSION TO {agent};
1346
+ ALTER USER {owner} GRANT CONNECT THROUGH {agent};
1347
+
1348
+ -- Cut the agent off later (owner untouched):
1349
+ -- ALTER USER {owner} REVOKE CONNECT THROUGH {agent};
1350
+ -- Fresh owner schema instead? See examples/agent-user-setup.example.sql.
1351
+ """
1352
+
1353
+
1335
1354
  CONVENTIONS_TEMPLATE = """{
1336
1355
  "naming": {
1337
1356
  "TABLE": "^T_[A-Z0-9_]+$",
@@ -1383,6 +1402,109 @@ than the warning they produce on every apply.
1383
1402
  """
1384
1403
 
1385
1404
 
1405
+ SCAN_SHARE = 0.9 # a token set must cover this much to count as the rule
1406
+ SCAN_MAX_ALTERNATIVES = 12
1407
+
1408
+
1409
+ def _dominant(tokens_at_position, total):
1410
+ """The smallest set of tokens covering SCAN_SHARE of the names, or None.
1411
+
1412
+ A set only counts as a rule when its tokens repeat. As many distinct
1413
+ tokens as there are names is a list of names, not a convention — and
1414
+ writing that down produces a pattern that fails on the next object
1415
+ anyone adds.
1416
+ """
1417
+ limit = min(SCAN_MAX_ALTERNATIVES, max(1, total // 2))
1418
+ counts = {}
1419
+ for tok in tokens_at_position:
1420
+ counts[tok] = counts.get(tok, 0) + 1
1421
+ ranked = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
1422
+ picked, covered = [], 0
1423
+ for tok, n in ranked:
1424
+ if len(picked) >= limit:
1425
+ return None
1426
+ picked.append(tok)
1427
+ covered += n
1428
+ if covered >= total * SCAN_SHARE:
1429
+ return picked
1430
+ return None
1431
+
1432
+
1433
+ def propose_pattern(names, min_names=3):
1434
+ """Read the shape off real names: a dominant first token, and a dominant
1435
+ last token when there is one. Returns None when the names share no
1436
+ structure — configuring nothing beats configuring a rule that means
1437
+ nothing."""
1438
+ names = [str(n).upper() for n in names if n]
1439
+ if len(names) < min_names:
1440
+ return None
1441
+ split = [n.split("_") for n in names]
1442
+ if sum(1 for p in split if len(p) >= 2) < len(names) * SCAN_SHARE:
1443
+ return None # mostly single-word names: no shape
1444
+ heads = _dominant([p[0] for p in split], len(names))
1445
+ if not heads:
1446
+ return None
1447
+ head = heads[0] if len(heads) == 1 else "(" + "|".join(sorted(heads)) + ")"
1448
+ # the suffixed form needs a third token to sit in; without one for most
1449
+ # names, an alternation at the end would exclude the short ones
1450
+ if sum(1 for p in split if len(p) >= 3) >= len(names) * SCAN_SHARE:
1451
+ tails = _dominant([p[-1] for p in split], len(names))
1452
+ if tails:
1453
+ tail = (tails[0] if len(tails) == 1
1454
+ else "(" + "|".join(sorted(tails)) + ")")
1455
+ return f"^{head}_[A-Z0-9_]+_{tail}$"
1456
+ return f"^{head}_[A-Z0-9_]+$"
1457
+
1458
+
1459
+ def scan_conventions(objects):
1460
+ """Propose a naming block from the schema itself. The tool tokenizes the
1461
+ names so the agent never has to read thousands of them into context; the
1462
+ developer's own document decides what is kept."""
1463
+ by_type = {}
1464
+ for otype, name in objects:
1465
+ by_type.setdefault(str(otype).upper(), []).append(name)
1466
+ naming = {}
1467
+ for otype, names in sorted(by_type.items()):
1468
+ pattern = propose_pattern(names)
1469
+ if pattern:
1470
+ naming[otype] = pattern
1471
+ return {"naming": naming}
1472
+
1473
+
1474
+ def pattern_coverage(conv, objects):
1475
+ """How well each configured pattern describes the names already in the
1476
+ schema. objects: (object_type, object_name).
1477
+
1478
+ A pattern derived from a document, or from reading a few examples, is a
1479
+ guess. Measuring it against every real name turns the guess into a number
1480
+ and names the exceptions — which is the difference between conventions
1481
+ that hold and conventions that produce a warning on every apply.
1482
+ """
1483
+ patterns = (conv or {}).get("naming") or {}
1484
+ out = {}
1485
+ for otype, pattern in patterns.items():
1486
+ rx = re.compile(pattern)
1487
+ names = sorted(n for t, n in objects if str(t).upper() == otype.upper())
1488
+ misses = [n for n in names if not rx.match(str(n))]
1489
+ out[otype] = {"pattern": pattern, "total": len(names),
1490
+ "matched": len(names) - len(misses), "misses": misses}
1491
+ return out
1492
+
1493
+
1494
+ def coverage_verdict(matched, total):
1495
+ """Read the numbers so nobody has to. A low rate means the pattern is
1496
+ wrong far more often than it means the schema is."""
1497
+ if total == 0:
1498
+ return "nothing of that type in this schema — untested rule"
1499
+ if matched == total:
1500
+ return "every name matches"
1501
+ pct = round(100 * matched / total)
1502
+ if pct >= 90:
1503
+ return f"{pct}% match — the rest are worth listing as exceptions"
1504
+ return (f"only {pct}% match — the derived pattern is probably wrong; "
1505
+ "widen it or split it before writing it down")
1506
+
1507
+
1386
1508
  def scaffold_conventions(root):
1387
1509
  """Write the conventions pair, skipping anything that already exists.
1388
1510
  Returns the paths actually created.
@@ -1861,6 +1983,11 @@ def cmd_journal(conn, schema, ns):
1861
1983
  sys.exit("unreachable: restore is dispatched with a connection in main")
1862
1984
 
1863
1985
 
1986
+ def _schema_objects(conn, schema):
1987
+ _, rows = run_query(conn, load_query("object-names.sql"), {"s": schema})
1988
+ return [(r[0], r[1]) for r in rows]
1989
+
1990
+
1864
1991
  def cmd_conventions(conn, schema, ns):
1865
1992
  root = ns.project_root
1866
1993
  if getattr(ns, "init", False):
@@ -1872,19 +1999,54 @@ def cmd_conventions(conn, schema, ns):
1872
1999
  f"{pathlib.Path(root) / CONFIG_DIR} — left untouched.")
1873
2000
  else:
1874
2001
  print(NL + "Edit the patterns to match this schema — "
1875
- f"`{invocation()} similar <A_TYPICAL_NAME>` shows what "
1876
- "it already does." + NL + "Every apply preview then "
1877
- "warns when a name drifts from them.")
2002
+ f"`{invocation()} conventions --scan` reads them off it.")
2003
+ return
2004
+
2005
+ if getattr(ns, "scan", False):
2006
+ proposed = scan_conventions(_schema_objects(conn, schema))
2007
+ if ns.json:
2008
+ print(json.dumps(proposed, indent=2))
2009
+ return
2010
+ print(f"Patterns read off {schema}. Review against your own standards, "
2011
+ "then save as" + NL + f"{CONFIG_DIR}/conventions.json:" + NL)
2012
+ print(json.dumps(proposed, indent=2))
2013
+ if not proposed["naming"]:
2014
+ print(NL + "Nothing proposed: too few objects, or names with no "
2015
+ "shared shape.")
2016
+ return
2017
+
2018
+ if getattr(ns, "check", False):
2019
+ conv = load_conventions(root)
2020
+ if not conv:
2021
+ sys.exit(f"No conventions to check. {invocation()} conventions "
2022
+ "--scan proposes some from this schema.")
2023
+ cov = pattern_coverage(conv, _schema_objects(conn, schema))
2024
+ if ns.json:
2025
+ print(json.dumps(cov, indent=2))
2026
+ return
2027
+ worst = 0
2028
+ for otype, s in sorted(cov.items()):
2029
+ print(f"{otype:<14} {s['matched']}/{s['total']} "
2030
+ f"{coverage_verdict(s['matched'], s['total'])}")
2031
+ if s["misses"]:
2032
+ shown, more = s["misses"][:8], len(s["misses"]) - 8
2033
+ print(" not matching: " + ", ".join(shown)
2034
+ + (f", +{more} more" if more > 0 else ""))
2035
+ worst = max(worst, len(s["misses"]))
2036
+ if worst:
2037
+ print(NL + "Every name above warns on `apply`. Widen the pattern "
2038
+ "if it is the rule that is wrong," + NL + "or record them in "
2039
+ "conventions.md as deliberate exceptions.")
1878
2040
  return
2041
+
1879
2042
  conv = load_conventions(root)
1880
2043
  if ns.json:
1881
2044
  print(json.dumps(conv or {}))
1882
2045
  return
1883
2046
  if not conv:
1884
2047
  print("No project conventions configured.")
1885
- print(f"Create the starter pair with: {invocation()} conventions --init")
1886
- print(" conventions.json naming patterns; apply previews warn on drift")
1887
- print(" conventions.md the prose rules your agents read first")
2048
+ print(f" {invocation()} conventions --scan read patterns off this schema")
2049
+ print(f" {invocation()} conventions --init start from a blank template")
1888
2050
  return
1889
2051
  print("Naming patterns — apply previews warn when a name drifts:")
1890
2052
  for otype, pattern in conv.get("naming", {}).items():
@@ -1892,28 +2054,7 @@ def cmd_conventions(conn, schema, ns):
1892
2054
  md = pathlib.Path(root) / CONFIG_DIR / "conventions.md"
1893
2055
  if md.is_file():
1894
2056
  print(NL + f"Prose rules for agents: {md}")
1895
- else:
1896
- print(NL + f"No conventions.md yet — {invocation()} conventions "
1897
- "--init writes one.")
1898
-
1899
-
1900
- AGENT_USER_SQL = """\
1901
- -- Least-privilege agent credential for schema {owner} — run as a DBA.
1902
- -- Proxy authentication: the agent logs in with its OWN password but works
1903
- -- inside {owner}; it never learns the owner password, owns nothing, and
1904
- -- revocation is one statement. Deliberately absent: DBA, RESOURCE, ANY
1905
- -- privileges, utility grants — the agent needs none of them to develop
1906
- -- PL/SQL, and every extra grant widens the blast radius.
1907
-
1908
- {create_line}
1909
- GRANT CREATE SESSION TO {agent};
1910
- ALTER USER {owner} GRANT CONNECT THROUGH {agent};
1911
-
1912
- -- Cut the agent off later (owner untouched):
1913
- -- ALTER USER {owner} REVOKE CONNECT THROUGH {agent};
1914
- -- Fresh owner schema instead? See examples/agent-user-setup.example.sql.
1915
- """
1916
-
2057
+ print(f"{NL}Measure them against the schema: {invocation()} conventions --check")
1917
2058
 
1918
2059
  def agent_user_sql(owner, agent, password, exists):
1919
2060
  """exists True: the user is already there — CREATE would be ORA-01920,
@@ -2244,7 +2385,7 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
2244
2385
  "unistr": cmd_unistr, "agent-user": cmd_agent_user,
2245
2386
  "history": cmd_history}
2246
2387
 
2247
- NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr",
2388
+ NO_DB_COMMANDS = {"policy", "journal", "install", "unistr",
2248
2389
  "history"}
2249
2390
 
2250
2391
 
@@ -2347,6 +2488,12 @@ def build_parser():
2347
2488
  s.add_argument("--init", action="store_true",
2348
2489
  help="write starter conventions.json and conventions.md "
2349
2490
  "into .pythia/ (never overwrites)")
2491
+ s.add_argument("--scan", action="store_true",
2492
+ help="read naming patterns off the live schema and "
2493
+ "propose them (needs a connection)")
2494
+ s.add_argument("--check", action="store_true",
2495
+ help="measure the configured patterns against the "
2496
+ "schema: coverage and the names that miss")
2350
2497
  s = sub.add_parser("agent-user", parents=[common()],
2351
2498
  help="SQL for a least-privilege proxy agent user; "
2352
2499
  "--save adds it to connections.json")
@@ -2405,7 +2552,11 @@ def main(argv=None):
2405
2552
  cwd = pathlib.Path.cwd()
2406
2553
  cfg, root = find_config(cwd, os.environ)
2407
2554
  ns.project_root = root if root is not None else cwd
2408
- if ns.command in NO_DB_COMMANDS and not (
2555
+ # conventions reads the schema only for --scan and --check; listing what
2556
+ # is configured, or writing the template, must work with no database at all
2557
+ offline_conventions = ns.command == "conventions" and not (
2558
+ getattr(ns, "scan", False) or getattr(ns, "check", False))
2559
+ if (ns.command in NO_DB_COMMANDS or offline_conventions) and not (
2409
2560
  ns.command == "journal" and getattr(ns, "action", "") == "restore"):
2410
2561
  COMMANDS[ns.command](None, None, ns)
2411
2562
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.4.10
3
+ Version: 0.5.0
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -178,13 +178,13 @@ it audits every interaction in `DBTOOLS$MCP_LOG`. **Writes never do** — only
178
178
 
179
179
  ## Skills
180
180
 
181
- Seven skills teach the agent the workflow — superpowers-style gates, not suggestions:
181
+ Eight skills teach the agent the workflow — gates, not suggestions:
182
182
 
183
- `pythia-setup` · `pythia-explore` · `pythia-impact` (impact **before** any change) ·
184
- `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate: the
185
- developer sees the preview and approves in chat before anything is written) ·
186
- `pythia-review` (seven antipatterns) · `pythia-skill-author` (capture *your team's*
187
- workflow as a new skill, mined from the live schema).
183
+ `pythia-setup` · `pythia-explore` · `pythia-impact` (before any change) ·
184
+ `pythia-write` (copy the codebase's conventions) · `pythia-apply` (the gate:
185
+ the developer approves the preview in chat) · `pythia-review` (antipatterns) ·
186
+ `pythia-conventions` (adopt a house style, verified against real names) ·
187
+ `pythia-skill-author` (capture your team's workflow as a skill).
188
188
 
189
189
  ## Compatibility
190
190
 
@@ -6,6 +6,7 @@ queries/dependencies.sql
6
6
  queries/impact.sql
7
7
  queries/invalid-objects.sql
8
8
  queries/name-occupants.sql
9
+ queries/object-names.sql
9
10
  queries/object-source.sql
10
11
  queries/plscope-enabled.sql
11
12
  queries/plscope-statements.sql
@@ -21,6 +22,7 @@ scripts/pythia_plsql.egg-info/entry_points.txt
21
22
  scripts/pythia_plsql.egg-info/requires.txt
22
23
  scripts/pythia_plsql.egg-info/top_level.txt
23
24
  skills/pythia-apply/SKILL.md
25
+ skills/pythia-conventions/SKILL.md
24
26
  skills/pythia-explore/SKILL.md
25
27
  skills/pythia-explore/reference/data-dictionary.md
26
28
  skills/pythia-impact/SKILL.md
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: pythia-conventions
3
+ description: Use when a project's house style already exists somewhere and needs to become something the tooling can check - the developer hands over a standards document, points at an older or company base schema whose naming should be adopted, says "our team does it this way", or asks why an apply preview warned about a name. Derives the patterns from the real schema, verifies them against it, and writes .pythia/conventions.json and conventions.md.
4
+ ---
5
+
6
+ # Adopting a Project's Conventions
7
+
8
+ **Announce at start:** "Using pythia-conventions — I'll read the patterns off the schema first."
9
+
10
+ Conventions already exist in every mature schema. They are in the object
11
+ names, in a standards document, or in a senior developer's head. This turns
12
+ them into two files: patterns the tool checks on every write, and prose every
13
+ future agent session reads before writing.
14
+
15
+ ## The Iron Law
16
+
17
+ ```
18
+ NO PATTERN WRITTEN DOWN BEFORE THE SCHEMA HAS AGREED WITH IT
19
+ ```
20
+
21
+ A pattern that does not match the objects already there is not a convention.
22
+ It is a guess that will warn on every apply until someone deletes it.
23
+
24
+ ## The Workflow
25
+
26
+ 1. **Read the shape off the database, not off your memory of it.**
27
+
28
+ ```bash
29
+ pythia conventions --scan
30
+ ```
31
+
32
+ It tokenises every object name and proposes a pattern per type. Do this
33
+ even when a document exists — and never page through thousands of names
34
+ yourself: that is what the command is for, and your context is better
35
+ spent elsewhere.
36
+
37
+ 2. **Read the developer's document, if there is one.** A base system's
38
+ standards, a wiki page, an older project's rules. Look for what a regex
39
+ cannot hold: parameter prefixes a calling layer depends on, a column every
40
+ query must filter by, where a transaction may commit, which date or money
41
+ representation is canonical. Those go in the prose half.
42
+
43
+ 3. **Reconcile, and say so when the two disagree.** The document states
44
+ intent; the schema states fact. A rule in the document that the schema
45
+ contradicts is one of three things — a rule nobody follows, a rule for new
46
+ code only, or drift worth reporting. Ask the developer which; do not
47
+ silently pick.
48
+
49
+ 4. **Write `.pythia/conventions.json`.** Start from the scan output, narrowed
50
+ by the document. `pythia conventions --init` writes a blank pair if you
51
+ want the skeleton first.
52
+
53
+ 5. **Verify — this step is the point of the skill.**
54
+
55
+ ```bash
56
+ pythia conventions --check
57
+ ```
58
+
59
+ It reports coverage per type and names what misses.
60
+
61
+ | Result | What it means |
62
+ |---|---|
63
+ | every name matches | the pattern is real; keep it |
64
+ | ~90%+ with a handful of misses | genuine exceptions — list them in step 6 |
65
+ | below 90% | **the pattern is wrong**, not the schema; widen or split it, then check again |
66
+ | nothing of that type | an untested rule; keep it only if new objects of that type are expected |
67
+
68
+ 6. **Record the exceptions, with their reason.** Objects that break the rule
69
+ on purpose — ported names from a base platform, legacy entry points whose
70
+ names are part of a public surface. Say why renaming would cost more than
71
+ the warning. An unexplained exception gets "fixed" by the next person.
72
+
73
+ 7. **Write `.pythia/conventions.md`.** For every rule, state **the cost of
74
+ breaking it**, not just the rule. "Parameter prefixes must be `b_`/`a_`"
75
+ is ignorable; "the calling layer binds by prefix, so a wrong one silently
76
+ unbinds the field" is not. Rules with named consequences get followed.
77
+
78
+ ## Adopting from an older base the team already runs
79
+
80
+ Point the connection at that schema and run steps 1 and 2 there — a base
81
+ system with thousands of objects is the most reliable statement of a house
82
+ style that exists, far better than anyone's recollection of it. Then switch
83
+ the connection to the new project and run `--check`: coverage tells you how
84
+ much of the base style the new schema has actually inherited.
85
+
86
+ ## Red Flags — STOP if you catch yourself thinking
87
+
88
+ | Thought | Reality |
89
+ |---------|---------|
90
+ | "I'll write the patterns from the document alone" | Documents describe intent; schemas record what happened. Scan first. |
91
+ | "80% coverage is good enough" | Every miss warns on every apply. Fix the pattern or record the exception. |
92
+ | "I'll read all the object names to work it out" | `--scan` does that without spending your context. |
93
+ | "The schema disagrees, so the document is wrong" | It may be a new-code-only rule. Ask. |
94
+ | "A rule per line is enough" | A rule without its cost is a rule people skip. |
95
+
96
+ ## When NOT to use this skill
97
+
98
+ - Writing one object in an existing style → `pythia-write`, which reads these
99
+ files and uses `similar` for the details.
100
+ - Capturing a *workflow* with steps and gates → `pythia-skill-author`.
101
+ Naming and style belong here; procedures belong in a skill.
@@ -114,6 +114,70 @@ def test_plscope_message_distinguishes_disabled_from_missing():
114
114
  assert "CALC_TAX" in missing
115
115
 
116
116
 
117
+ def test_pattern_coverage_measures_patterns_against_real_names():
118
+ """A derived pattern is a guess until the schema agrees. Coverage turns
119
+ 'I think procedures look like this' into a number, and names the
120
+ exceptions instead of leaving them to surface one apply at a time."""
121
+ conv = {"naming": {"PROCEDURE": "^P_[A-Z0-9_]+_(NH|LKE)$",
122
+ "FUNCTION": "^F_[A-Z0-9_]+$"}}
123
+ objects = [("PROCEDURE", "P_ORDER_NH"), ("PROCEDURE", "P_ORDER_LKE"),
124
+ ("PROCEDURE", "LEGACY_THING"), ("FUNCTION", "F_TAX"),
125
+ ("TABLE", "T_ORDER")] # no TABLE pattern: not reported
126
+ cov = pythia.pattern_coverage(conv, objects)
127
+ assert cov["PROCEDURE"]["matched"] == 2
128
+ assert cov["PROCEDURE"]["total"] == 3
129
+ assert cov["PROCEDURE"]["misses"] == ["LEGACY_THING"]
130
+ assert cov["FUNCTION"]["matched"] == 1 and cov["FUNCTION"]["misses"] == []
131
+ assert "TABLE" not in cov
132
+
133
+ # a pattern for a type the schema has none of is reported, not hidden:
134
+ # it is a rule nothing has tested yet
135
+ cov2 = pythia.pattern_coverage({"naming": {"TRIGGER": "^TRG_"}}, objects)
136
+ assert cov2["TRIGGER"]["total"] == 0
137
+
138
+
139
+ def test_coverage_verdict_reads_the_numbers_for_you():
140
+ assert "every" in pythia.coverage_verdict(7, 7).lower()
141
+ poor = pythia.coverage_verdict(2, 10)
142
+ assert "20%" in poor and "derived" in poor.lower() # suspect the pattern
143
+ assert "nothing of that type" in pythia.coverage_verdict(0, 0).lower()
144
+
145
+
146
+ def test_propose_pattern_reads_the_shape_off_real_names():
147
+ """The tool tokenizes thousands of names so the agent does not have to
148
+ read them. Dominant first and last tokens become alternations."""
149
+ names = ["PHT_NSD_LKE", "PHT_NSD_CT", "PHT_NSD_NH",
150
+ "PBH_HD_LKE", "PBH_HD_CT", "PBH_HD_NH"]
151
+ p = pythia.propose_pattern(names)
152
+ import re
153
+ assert all(re.match(p, n) for n in names), p
154
+ assert "PBH" in p and "PHT" in p and "LKE" in p # both ends captured
155
+
156
+ # no dominant suffix: leave the tail open rather than invent one
157
+ tables = ["HT_NSD", "HT_QUYEN", "HT_VAI_TRO", "HT_MA_BENH_VIEN"]
158
+ p2 = pythia.propose_pattern(tables)
159
+ assert all(re.match(p2, n) for n in tables), p2
160
+ assert p2.startswith("^HT_") and p2.endswith("$")
161
+
162
+ # nothing in common: say so with None rather than a pattern matching all
163
+ assert pythia.propose_pattern(["ALPHA", "B_TWO", "ZZZ_9"]) is None
164
+ assert pythia.propose_pattern([]) is None
165
+
166
+
167
+ def test_scan_proposal_covers_what_it_proposes():
168
+ """Whatever it proposes must match the names it was derived from —
169
+ otherwise the very first check would contradict the scan."""
170
+ objects = [("PROCEDURE", "PHT_A_LKE"), ("PROCEDURE", "PHT_B_CT"),
171
+ ("PROCEDURE", "PHT_C_NH"),
172
+ ("TABLE", "HT_A"), ("TABLE", "HT_B"), ("TABLE", "HT_C"),
173
+ ("SEQUENCE", "SEQ_ONE")] # too few to infer: left alone
174
+ conv = pythia.scan_conventions(objects)
175
+ cov = pythia.pattern_coverage(conv, objects)
176
+ for otype, stats in cov.items():
177
+ assert stats["misses"] == [], (otype, stats)
178
+ assert set(conv["naming"]) == {"PROCEDURE", "TABLE"}
179
+
180
+
117
181
  def main():
118
182
  failed = 0
119
183
  for name, fn in sorted(globals().items()):
@@ -12,7 +12,7 @@ SKILLS = ROOT / "skills"
12
12
 
13
13
  EXPECTED = {"pythia-setup", "pythia-explore", "pythia-impact",
14
14
  "pythia-write", "pythia-apply", "pythia-review",
15
- "pythia-skill-author"}
15
+ "pythia-skill-author", "pythia-conventions"}
16
16
 
17
17
  # spec: SKILL.md under 150 lines, detail pushed to reference/
18
18
  MAX_LINES = 150
File without changes
File without changes