brainpalace-cli 26.7.1__tar.gz → 26.7.2__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.
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/PKG-INFO +1 -1
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/ai_guidance.py +3 -1
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/client/api_client.py +59 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/hook.py +16 -2
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/query.py +71 -2
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/sync_docs.py +13 -1
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/data/ai_guidance.md +50 -12
- brainpalace_cli-26.7.2/brainpalace_cli/doc_sync/checkers/mode_parity_checker.py +35 -0
- brainpalace_cli-26.7.2/brainpalace_cli/doc_sync/checkers/modes.py +108 -0
- brainpalace_cli-26.7.2/brainpalace_cli/doc_sync/contract_parity.py +88 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/generator.py +29 -0
- brainpalace_cli-26.7.2/brainpalace_cli/doc_sync/mode_meta.py +93 -0
- brainpalace_cli-26.7.2/brainpalace_cli/doc_sync/mode_parity.py +70 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/serializer.py +38 -3
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/mcp_server/schemas.py +23 -2
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/mcp_server/server.py +2 -2
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/pyproject.toml +1 -1
- brainpalace_cli-26.7.1/brainpalace_cli/doc_sync/checkers/modes.py +0 -63
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/README.md +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/__main__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/cli.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/client/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/client/errors.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/_dashboard_url.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/ai_guide.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/backfill.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/bm25_project.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/cache.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/config.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/context.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/dashboard.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/doctor.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/estimate_util.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/extraction.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/extraction_drain.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/folders.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/graph.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/index.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/init.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/init_plan.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/inject.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/install_agent.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/jobs.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/list_cmd.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/lsp.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/mcp.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/memories.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/plugin_detect.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/read_only.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/reap.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/recall.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/records.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/remember.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/reset.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/session_hooks.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/sessions.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/start.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/status.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/stop.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/types.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/uninstall.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/update.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/verify_docs.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/commands/whoami.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config_fields.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config_migrate.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config_parity.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config_resolve.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config_review.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/config_schema.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/data/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/data/hooks/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/data/hooks/sessionstart-hook.sh +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/diagnostics.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/discovery.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/allowlist.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/base.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/cli_commands.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/config.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/endpoints.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/mcp.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/plugin_docs.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/provider_tables.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/checkers/skills.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/facts.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/introspect.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/markers.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/orchestrator.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/referential.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/doc_sync/triggers.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/known_projects.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/lsp_install.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/mcp_server/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/mcp_server/lifecycle.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/mcp_server/tools.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/migration.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/optional_deps.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/prompt_render.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/providers.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/__init__.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/claude_converter.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/codex_converter.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/converter_base.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/gemini_converter.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/opencode_converter.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/parser.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/skill_runtime_converter.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/tool_maps.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/runtime/types.py +0 -0
- {brainpalace_cli-26.7.1 → brainpalace_cli-26.7.2}/brainpalace_cli/xdg_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainpalace-cli
|
|
3
|
-
Version: 26.7.
|
|
3
|
+
Version: 26.7.2
|
|
4
4
|
Summary: BrainPalace CLI - Command-line interface for managing AI agent memory and knowledge retrieval
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: brainpalace,rag,cli,ai-memory,llm-memory,semantic-search,ai-agent,claude-code,agent-memory
|
|
@@ -43,9 +43,11 @@ _SKILL_FRONTMATTER = """\
|
|
|
43
43
|
name: using-brainpalace
|
|
44
44
|
description: |
|
|
45
45
|
Expert BrainPalace skill for document search with BM25 keyword, semantic
|
|
46
|
-
vector, hybrid, graph,
|
|
46
|
+
vector, hybrid, graph, multi, compute, scan, absence, and timeline
|
|
47
|
+
retrieval modes.
|
|
47
48
|
Use when asked to "search documentation", "query domain", "find in docs",
|
|
48
49
|
"bm25 search", "hybrid search", "semantic search", "graph search", "multi search",
|
|
50
|
+
"compute query", "scan sessions", "absence query", "timeline query",
|
|
49
51
|
"find dependencies", "code relationships", "searching knowledge base",
|
|
50
52
|
"querying indexed documents", "finding code references", "exploring codebase",
|
|
51
53
|
"what calls this function", "find imports", "trace dependencies",
|
|
@@ -100,6 +100,31 @@ class ScanRow:
|
|
|
100
100
|
score: float = 0.0
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
@dataclass
|
|
104
|
+
class AbsenceRow:
|
|
105
|
+
"""One absence-mode row (subject present under one partition, absent under
|
|
106
|
+
another)."""
|
|
107
|
+
|
|
108
|
+
label: str
|
|
109
|
+
present_in: str
|
|
110
|
+
absent_from: str
|
|
111
|
+
partition: str
|
|
112
|
+
score: float = 0.0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class TimelineRow:
|
|
117
|
+
"""One timeline-mode row (an entity's edge at a point in its history)."""
|
|
118
|
+
|
|
119
|
+
subject: str
|
|
120
|
+
predicate: str
|
|
121
|
+
object: str
|
|
122
|
+
valid_from: str | None = None
|
|
123
|
+
valid_until: str | None = None
|
|
124
|
+
valid: bool = True
|
|
125
|
+
score: float = 0.0
|
|
126
|
+
|
|
127
|
+
|
|
103
128
|
@dataclass
|
|
104
129
|
class QueryResponse:
|
|
105
130
|
"""Query response with results."""
|
|
@@ -109,6 +134,8 @@ class QueryResponse:
|
|
|
109
134
|
total_results: int
|
|
110
135
|
compute: list[ComputeRow] | None = None
|
|
111
136
|
scan: list[ScanRow] | None = None
|
|
137
|
+
absence: list[AbsenceRow] | None = None
|
|
138
|
+
timeline: list[TimelineRow] | None = None
|
|
112
139
|
index_blocked: dict[str, Any] | None = None
|
|
113
140
|
|
|
114
141
|
|
|
@@ -360,12 +387,44 @@ class DocServeClient:
|
|
|
360
387
|
for r in raw_scan
|
|
361
388
|
]
|
|
362
389
|
|
|
390
|
+
raw_absence = data.get("absence")
|
|
391
|
+
absence_rows: list[AbsenceRow] | None = None
|
|
392
|
+
if raw_absence is not None:
|
|
393
|
+
absence_rows = [
|
|
394
|
+
AbsenceRow(
|
|
395
|
+
label=str(r.get("label", "")),
|
|
396
|
+
present_in=str(r.get("present_in", "")),
|
|
397
|
+
absent_from=str(r.get("absent_from", "")),
|
|
398
|
+
partition=str(r.get("partition", "")),
|
|
399
|
+
score=float(r.get("score", 0.0)),
|
|
400
|
+
)
|
|
401
|
+
for r in raw_absence
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
raw_timeline = data.get("timeline")
|
|
405
|
+
timeline_rows: list[TimelineRow] | None = None
|
|
406
|
+
if raw_timeline is not None:
|
|
407
|
+
timeline_rows = [
|
|
408
|
+
TimelineRow(
|
|
409
|
+
subject=str(r.get("subject", "")),
|
|
410
|
+
predicate=str(r.get("predicate", "")),
|
|
411
|
+
object=str(r.get("object", "")),
|
|
412
|
+
valid_from=r.get("valid_from"),
|
|
413
|
+
valid_until=r.get("valid_until"),
|
|
414
|
+
valid=bool(r.get("valid", r.get("valid_until") is None)),
|
|
415
|
+
score=float(r.get("score", 0.0)),
|
|
416
|
+
)
|
|
417
|
+
for r in raw_timeline
|
|
418
|
+
]
|
|
419
|
+
|
|
363
420
|
return QueryResponse(
|
|
364
421
|
results=results,
|
|
365
422
|
query_time_ms=data.get("query_time_ms", 0.0),
|
|
366
423
|
total_results=data.get("total_results", len(results)),
|
|
367
424
|
compute=compute_rows,
|
|
368
425
|
scan=scan_rows,
|
|
426
|
+
absence=absence_rows,
|
|
427
|
+
timeline=timeline_rows,
|
|
369
428
|
index_blocked=data.get("index_blocked"),
|
|
370
429
|
)
|
|
371
430
|
|
|
@@ -171,7 +171,21 @@ def hook_userpromptsubmit() -> None:
|
|
|
171
171
|
# Query modes a guarded subagent prompt must reference to prove it will search
|
|
172
172
|
# via BrainPalace. Mirrors the modes accepted by `brainpalace query --mode` and
|
|
173
173
|
# the MCP/skill `query` tool's `mode` argument.
|
|
174
|
-
|
|
174
|
+
# The 9 query modes. Kept a CLI-local literal (NOT imported from the server enum)
|
|
175
|
+
# because hook.py is a fast, server-import-free shim loaded on every CLI call; the
|
|
176
|
+
# contract_parity gate (tests/doc_sync/test_mode_parity.py + lint:doc-sync) holds
|
|
177
|
+
# this tuple equal to brainpalace_server.models.query.QueryMode.
|
|
178
|
+
_GUARD_QUERY_MODES = (
|
|
179
|
+
"hybrid",
|
|
180
|
+
"bm25",
|
|
181
|
+
"vector",
|
|
182
|
+
"graph",
|
|
183
|
+
"multi",
|
|
184
|
+
"compute",
|
|
185
|
+
"scan",
|
|
186
|
+
"absence",
|
|
187
|
+
"timeline",
|
|
188
|
+
)
|
|
175
189
|
_GUARD_MODES_ALT = "|".join(_GUARD_QUERY_MODES)
|
|
176
190
|
# A prompt proves BrainPalace search two equivalent ways:
|
|
177
191
|
# CLI form — `brainpalace query ... --mode <mode>`
|
|
@@ -217,7 +231,7 @@ _GUARD_SEARCH_INTENT_RE = re.compile(
|
|
|
217
231
|
)
|
|
218
232
|
_GUARD_DENY_REASON = (
|
|
219
233
|
"Subagent prompt must instruct BrainPalace search: include "
|
|
220
|
-
"`brainpalace query ... --mode <
|
|
234
|
+
f"`brainpalace query ... --mode <{_GUARD_MODES_ALT}>`. "
|
|
221
235
|
"If genuinely exempt, open the prompt with a line "
|
|
222
236
|
"`# BRAINPALACE_EXEMPT: <reason of 20+ chars>`."
|
|
223
237
|
)
|
|
@@ -86,7 +86,17 @@ def _maybe_print_ai_hint() -> None:
|
|
|
86
86
|
"--mode",
|
|
87
87
|
default="hybrid",
|
|
88
88
|
type=click.Choice(
|
|
89
|
-
[
|
|
89
|
+
[
|
|
90
|
+
"vector",
|
|
91
|
+
"bm25",
|
|
92
|
+
"hybrid",
|
|
93
|
+
"graph",
|
|
94
|
+
"multi",
|
|
95
|
+
"compute",
|
|
96
|
+
"scan",
|
|
97
|
+
"absence",
|
|
98
|
+
"timeline",
|
|
99
|
+
],
|
|
90
100
|
case_sensitive=False,
|
|
91
101
|
),
|
|
92
102
|
help=(
|
|
@@ -98,7 +108,14 @@ def _maybe_print_ai_hint() -> None:
|
|
|
98
108
|
"unless record extraction has populated them), "
|
|
99
109
|
"'scan' (deterministic term counts over the archived session "
|
|
100
110
|
"transcripts; 'which week did I mention X most'; empty when the "
|
|
101
|
-
"session archive is off)
|
|
111
|
+
"session archive is off), "
|
|
112
|
+
"'absence' (anti-join over typed records: subjects present under one "
|
|
113
|
+
"partition value but absent under another, e.g. 'distance but not "
|
|
114
|
+
"duration'; empty when no two stored values resolve), "
|
|
115
|
+
"'timeline' (walk an entity's edge-validity/supersession history: "
|
|
116
|
+
"how a belief/fact evolved, e.g. 'how did the auth decision evolve'; "
|
|
117
|
+
"empty when the named entity resolves to no graph node), "
|
|
118
|
+
"Default: hybrid."
|
|
102
119
|
),
|
|
103
120
|
)
|
|
104
121
|
@click.option(
|
|
@@ -237,6 +254,18 @@ def query_command(
|
|
|
237
254
|
import dataclasses
|
|
238
255
|
|
|
239
256
|
output["scan"] = [dataclasses.asdict(s) for s in response.scan]
|
|
257
|
+
if response.absence is not None:
|
|
258
|
+
import dataclasses
|
|
259
|
+
|
|
260
|
+
output["absence"] = [
|
|
261
|
+
dataclasses.asdict(a) for a in response.absence
|
|
262
|
+
]
|
|
263
|
+
if response.timeline is not None:
|
|
264
|
+
import dataclasses
|
|
265
|
+
|
|
266
|
+
output["timeline"] = [
|
|
267
|
+
dataclasses.asdict(t) for t in response.timeline
|
|
268
|
+
]
|
|
240
269
|
blocked = getattr(response, "index_blocked", None)
|
|
241
270
|
if blocked is not None:
|
|
242
271
|
output["index_blocked"] = blocked
|
|
@@ -290,6 +319,46 @@ def query_command(
|
|
|
290
319
|
_maybe_print_ai_hint()
|
|
291
320
|
return
|
|
292
321
|
|
|
322
|
+
# Absence mode: print "in X, not Y" lines
|
|
323
|
+
if response.absence is not None:
|
|
324
|
+
console.print(
|
|
325
|
+
f"\n[bold]Query:[/] {query_text}\n"
|
|
326
|
+
f"[dim]Absence results in {response.query_time_ms:.1f}ms[/]\n"
|
|
327
|
+
)
|
|
328
|
+
if not response.absence:
|
|
329
|
+
console.print(
|
|
330
|
+
"[yellow]No gaps found (no two stored values resolved, "
|
|
331
|
+
"or nothing is absent).[/]"
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
for absence_row in response.absence:
|
|
335
|
+
console.print(
|
|
336
|
+
f"{absence_row.label} (in {absence_row.present_in}, "
|
|
337
|
+
f"not {absence_row.absent_from})"
|
|
338
|
+
)
|
|
339
|
+
_maybe_print_ai_hint()
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# Timeline mode: print entity edge-validity history lines
|
|
343
|
+
if response.timeline is not None:
|
|
344
|
+
if not response.timeline:
|
|
345
|
+
# M3: empty has two causes — no entity parsed from the query,
|
|
346
|
+
# or the entity resolved to no node / no edges. Cover both.
|
|
347
|
+
console.print(
|
|
348
|
+
"[yellow]No history found — the query named no "
|
|
349
|
+
"recognizable entity, or the entity resolved to no graph "
|
|
350
|
+
"node / has no edges. Try 'history of <name>'.[/]"
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
for tl_row in response.timeline:
|
|
354
|
+
mark = "[green]✓[/]" if tl_row.valid else "[dim]✗[/]"
|
|
355
|
+
until = tl_row.valid_until or "now"
|
|
356
|
+
console.print(
|
|
357
|
+
f"{mark} {tl_row.subject} —{tl_row.predicate}→ "
|
|
358
|
+
f"{tl_row.object} [{tl_row.valid_from or '?'} … {until}]"
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
|
|
293
362
|
# Display header
|
|
294
363
|
console.print(
|
|
295
364
|
f"\n[bold]Query:[/] {query_text}\n"
|
|
@@ -19,6 +19,7 @@ from brainpalace_cli.doc_sync.checkers.provider_tables import ProviderTablesChec
|
|
|
19
19
|
from brainpalace_cli.doc_sync.checkers.skills import SkillsChecker
|
|
20
20
|
from brainpalace_cli.doc_sync.generator import (
|
|
21
21
|
regenerate_mcp_tools,
|
|
22
|
+
regenerate_modes_block,
|
|
22
23
|
regenerate_provider_tables,
|
|
23
24
|
regenerate_query_modes,
|
|
24
25
|
)
|
|
@@ -43,6 +44,14 @@ _PLUGIN = _REPO_ROOT / "brainpalace-plugin"
|
|
|
43
44
|
# Roots scanned for provider/install GENERATED blocks (whole plugin tree + docs +
|
|
44
45
|
# both READMEs). Recursive — a new doc that adds a block is covered automatically.
|
|
45
46
|
_PROVIDER_DOC_ROOTS = [_PLUGIN, _DOCS, _REPO_ROOT / "README.md"]
|
|
47
|
+
# Doc -> mode-table style, for the additional (non-canonical) GENERATED:modes
|
|
48
|
+
# blocks: each doc keeps its own richer column shape, all rendered from the same
|
|
49
|
+
# MODE_META single source (see doc_sync/mode_meta.py).
|
|
50
|
+
_MODES_DOC_STYLES: dict[Path, str] = {
|
|
51
|
+
_REPO_ROOT / "README.md": "grid",
|
|
52
|
+
_DOCS / "API_REFERENCE.md": "table",
|
|
53
|
+
_DOCS / "USER_GUIDE.md": "commands",
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
|
|
48
57
|
def _require_repo() -> None:
|
|
@@ -77,7 +86,7 @@ def _provider_doc_files() -> list[Path]:
|
|
|
77
86
|
def _checkers() -> list[Any]:
|
|
78
87
|
return [
|
|
79
88
|
CliCommandsChecker(docs_dir=_DOCS_DIR),
|
|
80
|
-
ModesChecker(docs_dir=_DOCS_DIR),
|
|
89
|
+
ModesChecker(docs_dir=_DOCS_DIR, targets=_MODES_DOC_STYLES),
|
|
81
90
|
SkillsChecker(
|
|
82
91
|
skills_dir=_SKILLS_DIR,
|
|
83
92
|
docs_dir=_DOCS_DIR,
|
|
@@ -131,6 +140,9 @@ def sync_docs_command(mode_check: bool, mode_fix: bool) -> None:
|
|
|
131
140
|
regenerate_mcp_tools(_DOCS_DIR / MCP_CANONICAL, snap.mcp_tools)
|
|
132
141
|
for doc in _provider_doc_files():
|
|
133
142
|
regenerate_provider_tables(doc, snap)
|
|
143
|
+
for doc, style in _MODES_DOC_STYLES.items():
|
|
144
|
+
if doc.exists():
|
|
145
|
+
regenerate_modes_block(doc, snap.modes, style)
|
|
134
146
|
prose_needed = run_fix(checkers, snap)
|
|
135
147
|
for item in prose_needed:
|
|
136
148
|
click.echo(
|
|
@@ -53,6 +53,8 @@ Pick `--mode` for any inline search. Default to `hybrid` when unsure.
|
|
|
53
53
|
| Maximum recall (ALL usages / references) | `--mode multi` |
|
|
54
54
|
| Aggregation ("how many", "total", "which week had most") | `--mode compute` |
|
|
55
55
|
| Own-utterance history ("which week did I mention X") | `--mode scan` |
|
|
56
|
+
| Anti-join over typed records ("X but not Y") | `--mode absence` |
|
|
57
|
+
| Belief/fact evolution ("how did X evolve", "history of X") | `--mode timeline` |
|
|
56
58
|
|
|
57
59
|
Common flags: `--top-k N` (default 5), `--threshold F` (default 0.3),
|
|
58
60
|
`--alpha F` (hybrid balance, 0=BM25…1=Vector, default 0.5), `--language CODE`
|
|
@@ -97,18 +99,7 @@ Per-result keys are `text`, `source`, `score`, `chunk_id` — there is NO
|
|
|
97
99
|
`{"error": ...}` (with `detail`/`hint`) and a non-zero exit, with **no**
|
|
98
100
|
`results` key. Never append `2>/dev/null` — diagnostics go to stderr. A
|
|
99
101
|
top-level `index_blocked` object means the index is STALE (paused over
|
|
100
|
-
budget) — never auto-approve.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
brainpalace query "..." --mode hybrid --top-k 8 --json | python3 -c "
|
|
104
|
-
import json, sys
|
|
105
|
-
d = json.load(sys.stdin)
|
|
106
|
-
if 'error' in d: sys.exit('brainpalace error: %s' % d['error'])
|
|
107
|
-
for r in d['results']:
|
|
108
|
-
print(r['source'], r['score'])
|
|
109
|
-
print(r['text'][:500])
|
|
110
|
-
"
|
|
111
|
-
```
|
|
102
|
+
budget) — never auto-approve. (Runnable parsing snippet: `--tier full`.)
|
|
112
103
|
|
|
113
104
|
**Allowed Glob/Grep cases (NOT codebase search):** searching inside a single file
|
|
114
105
|
BrainPalace already returned; non-indexed paths (`~/.claude/`, `~/.config/`,
|
|
@@ -134,6 +125,21 @@ call the `ai_guide` tool).
|
|
|
134
125
|
|
|
135
126
|
---
|
|
136
127
|
|
|
128
|
+
## Parsing `--json` Output — runnable example
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
brainpalace query "..." --mode hybrid --top-k 8 --json | python3 -c "
|
|
132
|
+
import json, sys
|
|
133
|
+
d = json.load(sys.stdin)
|
|
134
|
+
if 'error' in d: sys.exit('brainpalace error: %s' % d['error'])
|
|
135
|
+
for r in d['results']:
|
|
136
|
+
print(r['source'], r['score'])
|
|
137
|
+
print(r['text'][:500])
|
|
138
|
+
"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
137
143
|
## Paused (Budget-Blocked) Indexing
|
|
138
144
|
|
|
139
145
|
Query output (CLI `--json` and the MCP `query` tool) may carry a top-level
|
|
@@ -225,6 +231,38 @@ is off or no term parses. Tie-break with compute: typed record metric wins.
|
|
|
225
231
|
|
|
226
232
|
---
|
|
227
233
|
|
|
234
|
+
## Absence Mode — Anti-Join over Typed Records
|
|
235
|
+
|
|
236
|
+
`--mode absence` — anti-join over typed records: subjects present under one
|
|
237
|
+
partition value (`metric`/`source`/`domain`) but absent under another
|
|
238
|
+
("distance but not duration"). Deterministic, no LLM. Empty when no two
|
|
239
|
+
stored values resolve, or nothing qualifies (e.g. no records exist).
|
|
240
|
+
Auto-routed after compute and scan — a query carrying a compute or scan tell
|
|
241
|
+
alongside an absence tell routes there instead (e.g. "did I discuss gmail but
|
|
242
|
+
not session" is scan, not absence, because it carries "did i discuss").
|
|
243
|
+
|
|
244
|
+
**`--json` contract for absence:** `results` is always `[]`; rows live under
|
|
245
|
+
`absence`. Each row: `label` (the missing subject), `present_in`,
|
|
246
|
+
`absent_from`, `partition` (`metric`/`source`/`domain`), `score` (reserved,
|
|
247
|
+
always `0.0`).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Timeline Mode — Edge-Validity / Supersession History
|
|
252
|
+
|
|
253
|
+
`timeline` — walk an entity's edge-validity/supersession history in the graph:
|
|
254
|
+
how a belief/fact evolved ("how did the auth decision evolve", "history of
|
|
255
|
+
auth.py"). Deterministic, no LLM. Requires the graph index (`ENABLE_GRAPH_INDEX`);
|
|
256
|
+
empty when the named entity resolves to no graph node or has no edges.
|
|
257
|
+
Auto-routed after compute, scan, and absence — a query carrying one of those
|
|
258
|
+
tells alongside a timeline tell routes there instead.
|
|
259
|
+
|
|
260
|
+
**`--json` contract for timeline:** `results` is always `[]`; rows live under
|
|
261
|
+
`timeline`. Each row: `subject`, `predicate`, `object`, `valid_from`,
|
|
262
|
+
`valid_until` (`null` = still valid), `valid`, `score`.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
228
266
|
## Session Memory — Optional, Separately Gated
|
|
229
267
|
|
|
230
268
|
Recall of prior AI-coding sessions (past-session transcript chunks and distilled
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Adapt the query_modes contract-parity result into doc-sync DriftRecords so the
|
|
2
|
+
lint:doc-sync gate fails on any cross-surface mode drift (both directions)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from brainpalace_cli.doc_sync.facts import DriftKind, DriftRecord, InterfaceSnapshot
|
|
7
|
+
from brainpalace_cli.doc_sync.mode_parity import query_modes_mismatches
|
|
8
|
+
|
|
9
|
+
_SURFACE_LOCATOR = {
|
|
10
|
+
"cli_choice": "brainpalace_cli/commands/query.py (--mode Choice)",
|
|
11
|
+
"mcp_literal": "brainpalace_cli/mcp_server/schemas.py (QueryMode)",
|
|
12
|
+
"hook_guard": "brainpalace_cli/commands/hook.py (_GUARD_QUERY_MODES)",
|
|
13
|
+
"mode_meta": "brainpalace_cli/doc_sync/mode_meta.py (MODE_META)",
|
|
14
|
+
"<sot>": "brainpalace_server/models/query.py (QueryMode)",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModeParityChecker:
|
|
19
|
+
surface = "mode-parity"
|
|
20
|
+
|
|
21
|
+
def check(self, snap: InterfaceSnapshot) -> list[DriftRecord]:
|
|
22
|
+
records: list[DriftRecord] = []
|
|
23
|
+
for m in query_modes_mismatches():
|
|
24
|
+
path = _SURFACE_LOCATOR.get(m.surface, m.surface)
|
|
25
|
+
if m.error is not None:
|
|
26
|
+
msg = f"{m.surface}: extractor error: {m.error}"
|
|
27
|
+
else:
|
|
28
|
+
msg = (
|
|
29
|
+
f"{m.surface} out of sync with server QueryMode enum: "
|
|
30
|
+
f"missing={sorted(m.missing)} extra={sorted(m.extra)}"
|
|
31
|
+
)
|
|
32
|
+
records.append(
|
|
33
|
+
DriftRecord(self.surface, m.surface, path, DriftKind.MISMATCH, msg)
|
|
34
|
+
)
|
|
35
|
+
return records
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Modes surface: canonical GENERATED:modes block in brainpalace-query.md (add/remove
|
|
2
|
+
gate) + a scoped referential check for `--mode <token>` invocations elsewhere +
|
|
3
|
+
MODE_META coverage + any additional per-doc `targets` (README/docs mode tables in
|
|
4
|
+
their own richer shape, e.g. README's grid or USER_GUIDE's commands table)."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from brainpalace_cli.doc_sync.facts import DriftKind, DriftRecord, InterfaceSnapshot
|
|
12
|
+
from brainpalace_cli.doc_sync.generator import MODES_RENDERERS
|
|
13
|
+
from brainpalace_cli.doc_sync.markers import MarkerError, find_block
|
|
14
|
+
from brainpalace_cli.doc_sync.mode_meta import MODE_META
|
|
15
|
+
from brainpalace_cli.doc_sync.referential import dangling_tokens
|
|
16
|
+
from brainpalace_cli.doc_sync.serializer import render_modes_table
|
|
17
|
+
|
|
18
|
+
SURFACE = "modes"
|
|
19
|
+
CANONICAL = "brainpalace-query.md"
|
|
20
|
+
# Scoped to real invocations: `--mode <tok>` or `--mode=<tok>` (resolution H).
|
|
21
|
+
_MODE_RE = re.compile(r"--mode[= ]([a-z][a-z0-9-]*)")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModesChecker:
|
|
25
|
+
surface = SURFACE
|
|
26
|
+
|
|
27
|
+
def __init__(self, docs_dir: Path, targets: dict[Path, str] | None = None) -> None:
|
|
28
|
+
self.docs_dir = Path(docs_dir)
|
|
29
|
+
# path -> style ("table"/"grid"/"commands"): additional docs whose own
|
|
30
|
+
# GENERATED:modes block (if present) is checked against that doc's style.
|
|
31
|
+
self.targets = {Path(p): style for p, style in (targets or {}).items()}
|
|
32
|
+
|
|
33
|
+
def check(self, snap: InterfaceSnapshot) -> list[DriftRecord]:
|
|
34
|
+
records: list[DriftRecord] = []
|
|
35
|
+
missing_meta = [m for m in snap.modes if m not in MODE_META]
|
|
36
|
+
if missing_meta:
|
|
37
|
+
records.append(
|
|
38
|
+
DriftRecord(
|
|
39
|
+
SURFACE,
|
|
40
|
+
"mode-meta",
|
|
41
|
+
"brainpalace_cli/doc_sync/mode_meta.py",
|
|
42
|
+
DriftKind.MISSING,
|
|
43
|
+
f"MODE_META has no entry for live mode(s): {missing_meta!r}",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
# A mode lacking MODE_META already yields a MISSING record above; every
|
|
47
|
+
# render_modes_* call raises ValueError for it, so skip the render-based
|
|
48
|
+
# comparisons below rather than crash the whole checker on that gap.
|
|
49
|
+
canon = self.docs_dir / CANONICAL
|
|
50
|
+
if canon.exists():
|
|
51
|
+
text = canon.read_text(encoding="utf-8")
|
|
52
|
+
try:
|
|
53
|
+
inner = find_block(text, "modes")
|
|
54
|
+
if (
|
|
55
|
+
not missing_meta
|
|
56
|
+
and inner.strip() != render_modes_table(snap.modes).strip()
|
|
57
|
+
):
|
|
58
|
+
records.append(
|
|
59
|
+
DriftRecord(
|
|
60
|
+
SURFACE,
|
|
61
|
+
"query",
|
|
62
|
+
str(canon),
|
|
63
|
+
DriftKind.MISMATCH,
|
|
64
|
+
"canonical modes block out of sync with --mode Choice",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
except MarkerError:
|
|
68
|
+
records.append(
|
|
69
|
+
DriftRecord(
|
|
70
|
+
SURFACE,
|
|
71
|
+
"query",
|
|
72
|
+
str(canon),
|
|
73
|
+
DriftKind.MISSING,
|
|
74
|
+
"canonical GENERATED:modes block absent",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
if not missing_meta:
|
|
78
|
+
for path, style in self.targets.items():
|
|
79
|
+
if not path.exists():
|
|
80
|
+
continue
|
|
81
|
+
text = path.read_text(encoding="utf-8")
|
|
82
|
+
try:
|
|
83
|
+
inner = find_block(text, "modes")
|
|
84
|
+
except MarkerError:
|
|
85
|
+
continue # block not present in this doc — nothing to gate
|
|
86
|
+
render = MODES_RENDERERS[style]
|
|
87
|
+
if inner.strip() != render(snap.modes).strip():
|
|
88
|
+
records.append(
|
|
89
|
+
DriftRecord(
|
|
90
|
+
SURFACE,
|
|
91
|
+
path.stem,
|
|
92
|
+
str(path),
|
|
93
|
+
DriftKind.MISMATCH,
|
|
94
|
+
f"GENERATED:modes block ({style}) out of sync with "
|
|
95
|
+
"--mode Choice",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
all_docs = sorted(self.docs_dir.glob("brainpalace-*.md"))
|
|
99
|
+
records.extend(
|
|
100
|
+
dangling_tokens(
|
|
101
|
+
all_docs,
|
|
102
|
+
_MODE_RE,
|
|
103
|
+
set(snap.modes),
|
|
104
|
+
SURFACE,
|
|
105
|
+
"`--mode {tok}` references a non-existent mode",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return records
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Generic cross-surface parity gate.
|
|
2
|
+
|
|
3
|
+
A *contract* is one canonical token set (the source of truth) plus a set of named
|
|
4
|
+
*surfaces* that each independently re-declare that set. The check asserts every
|
|
5
|
+
surface equals the SoT, in BOTH directions (a surface missing a token, or carrying
|
|
6
|
+
an extra one), and fails loud per surface (an extractor that raises is reported by
|
|
7
|
+
name, never crashes the run). Register a contract once; `check_all` loops them all.
|
|
8
|
+
This kills the "same list hardcoded in N places drifts apart" bug class for any
|
|
9
|
+
future contract, not just query modes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Contract:
|
|
20
|
+
name: str
|
|
21
|
+
sot: Callable[[], set[str]]
|
|
22
|
+
surfaces: dict[str, Callable[[], set[str]]] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ParityMismatch:
|
|
27
|
+
contract: str
|
|
28
|
+
surface: str
|
|
29
|
+
missing: frozenset[str] = frozenset() # in SoT, absent from the surface
|
|
30
|
+
extra: frozenset[str] = frozenset() # in the surface, absent from the SoT
|
|
31
|
+
error: str | None = None # the extractor (or SoT) raised
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_CONTRACTS: dict[str, Contract] = {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register_contract(
|
|
38
|
+
name: str,
|
|
39
|
+
sot: Callable[[], set[str]],
|
|
40
|
+
surfaces: dict[str, Callable[[], set[str]]],
|
|
41
|
+
) -> None:
|
|
42
|
+
_CONTRACTS[name] = Contract(name, sot, dict(surfaces))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clear_contracts() -> None:
|
|
46
|
+
_CONTRACTS.clear()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_contract(name: str) -> list[ParityMismatch]:
|
|
50
|
+
c = _CONTRACTS[name]
|
|
51
|
+
try:
|
|
52
|
+
truth = c.sot()
|
|
53
|
+
except Exception as exc: # noqa: BLE001 - report, don't crash the gate
|
|
54
|
+
return [ParityMismatch(c.name, "<sot>", error=repr(exc))]
|
|
55
|
+
out: list[ParityMismatch] = []
|
|
56
|
+
for surface, extract in c.surfaces.items():
|
|
57
|
+
try:
|
|
58
|
+
got = extract()
|
|
59
|
+
except Exception as exc: # noqa: BLE001
|
|
60
|
+
out.append(ParityMismatch(c.name, surface, error=repr(exc)))
|
|
61
|
+
continue
|
|
62
|
+
missing = frozenset(truth - got)
|
|
63
|
+
extra = frozenset(got - truth)
|
|
64
|
+
if missing or extra:
|
|
65
|
+
out.append(ParityMismatch(c.name, surface, missing=missing, extra=extra))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_all() -> list[ParityMismatch]:
|
|
70
|
+
out: list[ParityMismatch] = []
|
|
71
|
+
for name in _CONTRACTS:
|
|
72
|
+
out.extend(check_contract(name))
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_mismatches(mismatches: list[ParityMismatch]) -> str:
|
|
77
|
+
if not mismatches:
|
|
78
|
+
return "contract parity OK"
|
|
79
|
+
lines = ["contract parity FAILED:"]
|
|
80
|
+
for m in mismatches:
|
|
81
|
+
if m.error is not None:
|
|
82
|
+
lines.append(f" [{m.contract}] {m.surface}: extractor error: {m.error}")
|
|
83
|
+
else:
|
|
84
|
+
lines.append(
|
|
85
|
+
f" [{m.contract}] {m.surface}: "
|
|
86
|
+
f"missing={sorted(m.missing)} extra={sorted(m.extra)}"
|
|
87
|
+
)
|
|
88
|
+
return "\n".join(lines)
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
import re
|
|
8
8
|
import subprocess
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Callable
|
|
10
11
|
|
|
11
12
|
from brainpalace_cli.doc_sync.facts import CommandFact, InterfaceSnapshot
|
|
12
13
|
from brainpalace_cli.doc_sync.markers import (
|
|
@@ -20,10 +21,20 @@ from brainpalace_cli.doc_sync.serializer import (
|
|
|
20
21
|
render_flags_section,
|
|
21
22
|
render_flags_table,
|
|
22
23
|
render_mcp_tools_table,
|
|
24
|
+
render_modes_commands,
|
|
25
|
+
render_modes_grid,
|
|
23
26
|
render_modes_table,
|
|
24
27
|
render_params_yaml,
|
|
25
28
|
)
|
|
26
29
|
|
|
30
|
+
#: style name -> renderer(modes) -> markdown body. Each target doc registers the
|
|
31
|
+
#: style matching its own richer column shape; all pull from the same MODE_META.
|
|
32
|
+
MODES_RENDERERS: dict[str, Callable[[list[str]], str]] = {
|
|
33
|
+
"table": render_modes_table,
|
|
34
|
+
"grid": render_modes_grid,
|
|
35
|
+
"commands": render_modes_commands,
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
_PARAMS_RE = re.compile(r"^parameters:.*?(?=^\S|\Z)", re.MULTILINE | re.DOTALL)
|
|
28
39
|
|
|
29
40
|
|
|
@@ -117,6 +128,24 @@ def regenerate_query_modes(path: Path, modes: list[str]) -> None:
|
|
|
117
128
|
path.write_text(text, encoding="utf-8")
|
|
118
129
|
|
|
119
130
|
|
|
131
|
+
def regenerate_modes_block(path: Path, modes: list[str], style: str) -> bool:
|
|
132
|
+
"""Refresh the GENERATED:modes block in ``path`` IF PRESENT, using the renderer
|
|
133
|
+
for ``style`` (mirrors regenerate_provider_tables: these tables are placed by a
|
|
134
|
+
human where they belong, never auto-appended). Returns True if the file changed,
|
|
135
|
+
False when the file has no modes block (skipped, untouched)."""
|
|
136
|
+
render = MODES_RENDERERS[style]
|
|
137
|
+
text = path.read_text(encoding="utf-8")
|
|
138
|
+
try:
|
|
139
|
+
find_block(text, "modes")
|
|
140
|
+
except MarkerError:
|
|
141
|
+
return False
|
|
142
|
+
new_text = replace_block(text, "modes", render(modes))
|
|
143
|
+
if new_text != text:
|
|
144
|
+
path.write_text(new_text, encoding="utf-8")
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
120
149
|
def regenerate_provider_tables(path: Path, snap: InterfaceSnapshot) -> bool:
|
|
121
150
|
"""Refresh every provider/install GENERATED block PRESENT in ``path`` from the
|
|
122
151
|
live registry. Only existing blocks are rewritten (these tables are placed by a
|