@luizsantiago/spec-guardrails 3.2.0 → 3.2.1

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.
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ """Bounded knowledge-graph traversal over the SQLite memory index.
3
+
4
+ python3 memory_query.py --from T1 --depth 2
5
+ python3 memory_query.py --from REQ-001 --depth 1 --json
6
+
7
+ Exit codes: 0 ok, 1 failure, 2 usage error.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sqlite3
15
+ import sys
16
+ from collections import deque
17
+ from pathlib import Path
18
+
19
+ from _common import EXIT_FAILED, EXIT_OK, EXIT_USAGE
20
+
21
+ GATE = "memory-query"
22
+ DB_PATH = Path(".specs/memory/memory.db")
23
+ DEFAULT_DEPTH = 2
24
+ MAX_DEPTH = 5
25
+
26
+
27
+ def fail(message: str, code: int = EXIT_FAILED) -> int:
28
+ print(f"[{GATE}] FAIL - {DB_PATH}")
29
+ print(f" error {message}")
30
+ return code
31
+
32
+
33
+ def normalize_entity_id(raw: str) -> str:
34
+ value = raw.strip()
35
+ if value.upper().startswith("T") and value[1:].isdigit():
36
+ return f"T{int(value[1:])}"
37
+ return value
38
+
39
+
40
+ def fetch_entity(conn: sqlite3.Connection, entity_id: str) -> dict | None:
41
+ row = conn.execute(
42
+ "SELECT id, kind, label, source_path, updated_at FROM entities WHERE id = ?",
43
+ (entity_id,),
44
+ ).fetchone()
45
+ if not row:
46
+ return None
47
+ return {
48
+ "id": row[0],
49
+ "kind": row[1],
50
+ "label": row[2],
51
+ "source_path": row[3],
52
+ "updated_at": row[4],
53
+ }
54
+
55
+
56
+ def fetch_neighbors(conn: sqlite3.Connection, entity_id: str) -> list[dict]:
57
+ rows = conn.execute(
58
+ """
59
+ SELECT r.kind, e.id, e.kind, e.label, e.source_path
60
+ FROM relations r
61
+ JOIN entities e ON e.id = r.to_id
62
+ WHERE r.from_id = ?
63
+ UNION
64
+ SELECT r.kind, e.id, e.kind, e.label, e.source_path
65
+ FROM relations r
66
+ JOIN entities e ON e.id = r.from_id
67
+ WHERE r.to_id = ?
68
+ """,
69
+ (entity_id, entity_id),
70
+ ).fetchall()
71
+
72
+ neighbors: list[dict] = []
73
+ for kind, nid, nkind, label, source_path in rows:
74
+ neighbors.append(
75
+ {
76
+ "relation": kind,
77
+ "entity": {
78
+ "id": nid,
79
+ "kind": nkind,
80
+ "label": label,
81
+ "source_path": source_path,
82
+ },
83
+ }
84
+ )
85
+ return neighbors
86
+
87
+
88
+ def traverse(from_id: str, depth: int) -> dict:
89
+ if not DB_PATH.is_file():
90
+ raise FileNotFoundError(
91
+ f"{DB_PATH} not found — run `memory-index rebuild` after install"
92
+ )
93
+
94
+ conn = sqlite3.connect(DB_PATH)
95
+ try:
96
+ root_id = normalize_entity_id(from_id)
97
+ root = fetch_entity(conn, root_id)
98
+ if not root:
99
+ raise LookupError(f"entity not found: {root_id}")
100
+
101
+ visited: set[str] = {root_id}
102
+ edges: list[dict] = []
103
+ entities: dict[str, dict] = {root_id: root}
104
+ queue: deque[tuple[str, int]] = deque([(root_id, 0)])
105
+
106
+ while queue:
107
+ current_id, current_depth = queue.popleft()
108
+ if current_depth >= depth:
109
+ continue
110
+
111
+ for neighbor in fetch_neighbors(conn, current_id):
112
+ target = neighbor["entity"]
113
+ target_id = target["id"]
114
+ edges.append(
115
+ {
116
+ "from": current_id,
117
+ "to": target_id,
118
+ "kind": neighbor["relation"],
119
+ }
120
+ )
121
+ if target_id not in entities:
122
+ entities[target_id] = target
123
+ if target_id not in visited:
124
+ visited.add(target_id)
125
+ queue.append((target_id, current_depth + 1))
126
+
127
+ return {
128
+ "from": root_id,
129
+ "depth": depth,
130
+ "entities": list(entities.values()),
131
+ "relations": edges,
132
+ }
133
+ finally:
134
+ conn.close()
135
+
136
+
137
+ def cmd_query(args: argparse.Namespace) -> int:
138
+ depth = args.depth if args.depth is not None else DEFAULT_DEPTH
139
+ if depth < 0 or depth > MAX_DEPTH:
140
+ return fail(f"--depth must be between 0 and {MAX_DEPTH}", EXIT_USAGE)
141
+
142
+ try:
143
+ package = traverse(args.from_id, depth)
144
+ except FileNotFoundError as err:
145
+ return fail(str(err))
146
+ except LookupError as err:
147
+ return fail(str(err))
148
+
149
+ if args.json:
150
+ print(json.dumps(package, indent=2))
151
+ else:
152
+ print(f"[{GATE}] context package from {package['from']} (depth {package['depth']})")
153
+ print(f" entities: {len(package['entities'])}")
154
+ print(f" relations: {len(package['relations'])}")
155
+ for entity in package["entities"]:
156
+ print(f" - {entity['id']} ({entity['kind']}): {entity['label']}")
157
+
158
+ return EXIT_OK
159
+
160
+
161
+ def build_parser() -> argparse.ArgumentParser:
162
+ parser = argparse.ArgumentParser(description="Query the SQLite knowledge graph")
163
+ parser.add_argument("--from", dest="from_id", required=True, help="root entity id")
164
+ parser.add_argument("--depth", type=int, default=DEFAULT_DEPTH)
165
+ parser.add_argument("--json", action="store_true")
166
+ parser.set_defaults(func=cmd_query)
167
+ return parser
168
+
169
+
170
+ def main(argv: list[str] | None = None) -> int:
171
+ parser = build_parser()
172
+ args = parser.parse_args(argv)
173
+ return args.func(args)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ sys.exit(main())
@@ -311,6 +311,7 @@ def build_report(target: str, text: str) -> Report:
311
311
  "MODIFIED",
312
312
  )
313
313
  validate_removed_section(report, visible)
314
+ validate_out_of_scope(report, visible)
314
315
  else:
315
316
  for section in REQUIRED_SECTIONS:
316
317
  if has_section(visible, section):
@@ -46,6 +46,10 @@ Structural gates run **before** owner review, so they cannot drift when the mode
46
46
  | Before approving tasks | `python3 .specs/guardrails/scripts/analyze_artifacts.py [feature]` |
47
47
  | Before presenting tasks for approval | `python3 .specs/guardrails/scripts/validate_tasks.py [feature]` |
48
48
  | Before Execute waves (3+ tasks) | `npx @luizsantiago/spec-guardrails loop-plan [feature]` |
49
+ | Parallel wave (2+ tasks, disjoint Files) | `npx @luizsantiago/spec-guardrails workspace-prepare [feature] --tasks T1,T2` |
50
+ | After parallel wave merge | `npx @luizsantiago/spec-guardrails workspace-cleanup [feature] --force` |
51
+ | Before editing paths outside task Files | `npx @luizsantiago/spec-guardrails execution-policy check-path <path>` |
52
+ | On gate retry (Execute playbook) | `npx @luizsantiago/spec-guardrails execution-policy record-retry Tn` |
49
53
  | On each commit | `python3 .specs/guardrails/scripts/check_commit.py --message "<message>"` |
50
54
  | Before declaring a feature done | `python3 .specs/guardrails/scripts/validate_state.py [feature]` |
51
55
  | Traceability (Medium+ features) | `python3 .specs/guardrails/scripts/validate_traceability.py [feature]` |
@@ -62,6 +62,21 @@ node_modules/
62
62
 
63
63
  Follow `engineering-standards.md` — separate commits for code vs docs when possible.
64
64
 
65
+ ## Execution policy (scope and budgets)
66
+
67
+ When `.specs/config.yaml` defines `budget`, `scope`, or `escalation`, consult policy before expanding edits:
68
+
69
+ ```bash
70
+ npx @luizsantiago/spec-guardrails execution-policy status
71
+ npx @luizsantiago/spec-guardrails execution-policy check-path src/auth.ts
72
+ ```
73
+
74
+ - **Scope:** denied paths block with exit 1; allowed_paths (when set) restrict edits to listed globs.
75
+ - **Retries:** after a task gate failure, run `execution-policy record-retry Tn`; stop at `max_retries_per_task` (default 3) and escalate per the Execute playbook — do not bypass with `--no-verify`.
76
+ - **Agent runs:** orchestrators may call `execution-policy record-run` when dispatching sub-agents; stop when budgets exhaust.
77
+
78
+ Policy is soft-enforced via CLI + skills — not an OS sandbox. Escalation defaults: scope expansion → human review; budget exhaustion → stop.
79
+
65
80
  ## Git Blast Radius (Tiers)
66
81
 
67
82
  Structural gates enforce artifact quality. Git tiers enforce **what leaves the machine**.
@@ -1,18 +1,20 @@
1
1
  # Lessons
2
2
 
3
- How to record, promote, and load grounded lessons. The engine owns the files; this reference owns the judgment.
3
+ How to record, promote, graduate, and load grounded lessons. The engine owns the files; this reference owns the judgment.
4
4
 
5
5
  ## When to Use
6
6
 
7
7
  - The last step of `/verify` when the verdict is FAIL for a grounded reason
8
- - The first step of `/specify` and `/plan`, loading only confirmed lessons
9
- - After a confirmed lesson was loaded and the same failure still happened (`penalize`)
8
+ - The first step of `/specify` and `/plan`, loading only approved lessons
9
+ - After a approved lesson was loaded and the same failure still happened (`penalize`)
10
+ - When a stable approved rule should become an engineering standard (`graduate` — human only)
10
11
 
11
12
  ## When NOT to Use
12
13
 
13
14
  - A clean PASS — record nothing
14
15
  - A preference, a style nit, or anything not evidenced in `validation.md`
15
16
  - Hand-editing `.specs/LESSONS.md` or `.specs/lessons.json`
17
+ - **Auto-graduation** — agents must never graduate without explicit human approval
16
18
 
17
19
  ## Files
18
20
 
@@ -20,7 +22,7 @@ How to record, promote, and load grounded lessons. The engine owns the files; th
20
22
  | --- | --- | --- |
21
23
  | `.specs/lessons.json` | `lessons.py` | Canonical store |
22
24
  | `.specs/LESSONS.md` | `lessons.py` | Rendered playbook — read, never write |
23
- | `.specs/guardrails/scripts/lessons.py` | Harness | `add`, `list`, `penalize`, `prune`, `status` |
25
+ | `.specs/guardrails/scripts/lessons.py` | Harness | `add`, `list`, `promote`, `graduate`, `penalize`, `prune`, `status` |
24
26
 
25
27
  ## How to Phrase
26
28
 
@@ -42,25 +44,29 @@ Titles and rules are deduped after normalization (casefold, accents stripped, pu
42
44
 
43
45
  ```
44
46
  FAIL with evidence → add (candidate)
45
- candidate seen in 2 distinct features → confirmed (guidance)
46
- confirmed loaded but the same failure recurs → penalize
47
+ observed → repeated → candidate → approved → graduated → deprecated
48
+ candidate seen in 2 distinct features → approved (automatic)
49
+ approved loaded but the same failure recurs → penalize
47
50
  2 penalties → quarantined (stop loading)
48
51
  candidate idle 90 days → prune
52
+ graduate (human) → approved → graduated with --evidence + validation.md
49
53
  ```
50
54
 
51
- Same-feature recurrence does not promote. One noisy feature must not turn a guess into a house rule.
55
+ Same-feature recurrence does not promote. One noisy feature must not turn a guess into a house rule. **Never auto-graduate** — use `graduate` only after human review.
56
+
57
+ Manual promotion (`promote --id L-001`) advances one step along `observed → repeated → candidate → approved` when corroboration is incomplete.
52
58
 
53
59
  ## When to Load
54
60
 
55
61
  At the start of Specify and Design:
56
62
 
57
63
  ```bash
58
- python3 .specs/guardrails/scripts/lessons.py list --status confirmed
64
+ python3 .specs/guardrails/scripts/lessons.py list --status approved
59
65
  ```
60
66
 
61
- Apply every confirmed rule that matches this work. Candidates are not guidance — they live in `lessons.json` until a second feature corroborates them.
67
+ Apply every approved rule that matches this work. Candidates are not guidance — they live in `lessons.json` until a second feature corroborates them or a human promotes them.
62
68
 
63
- If a confirmed lesson was in the working set and the same gap still appears in `/verify`, penalize it with the new `validation.md` as `--source`. Two penalties quarantine it.
69
+ If an approved lesson was in the working set and the same gap still appears in `/verify`, penalize it with the new `validation.md` as `--source`. Two penalties quarantine it.
64
70
 
65
71
  ## Degraded mode
66
72