sqlserver-semantic-mcp 0.5.0__py3-none-any.whl
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.
- sqlserver_semantic_mcp/__init__.py +1 -0
- sqlserver_semantic_mcp/config.py +78 -0
- sqlserver_semantic_mcp/domain/__init__.py +0 -0
- sqlserver_semantic_mcp/domain/enums.py +48 -0
- sqlserver_semantic_mcp/domain/models/__init__.py +0 -0
- sqlserver_semantic_mcp/domain/models/column.py +14 -0
- sqlserver_semantic_mcp/domain/models/object.py +13 -0
- sqlserver_semantic_mcp/domain/models/relationship.py +11 -0
- sqlserver_semantic_mcp/domain/models/table.py +29 -0
- sqlserver_semantic_mcp/infrastructure/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/background.py +59 -0
- sqlserver_semantic_mcp/infrastructure/cache/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/cache/semantic.py +132 -0
- sqlserver_semantic_mcp/infrastructure/cache/store.py +152 -0
- sqlserver_semantic_mcp/infrastructure/cache/structural.py +203 -0
- sqlserver_semantic_mcp/infrastructure/connection.py +78 -0
- sqlserver_semantic_mcp/infrastructure/queries/__init__.py +0 -0
- sqlserver_semantic_mcp/infrastructure/queries/comment_queries.py +18 -0
- sqlserver_semantic_mcp/infrastructure/queries/metadata_queries.py +70 -0
- sqlserver_semantic_mcp/infrastructure/queries/object_queries.py +15 -0
- sqlserver_semantic_mcp/main.py +90 -0
- sqlserver_semantic_mcp/policy/__init__.py +0 -0
- sqlserver_semantic_mcp/policy/analyzer.py +194 -0
- sqlserver_semantic_mcp/policy/enforcer.py +104 -0
- sqlserver_semantic_mcp/policy/intents/__init__.py +16 -0
- sqlserver_semantic_mcp/policy/intents/ast_analyzer.py +24 -0
- sqlserver_semantic_mcp/policy/intents/base.py +17 -0
- sqlserver_semantic_mcp/policy/intents/regex_analyzer.py +11 -0
- sqlserver_semantic_mcp/policy/intents/router.py +21 -0
- sqlserver_semantic_mcp/policy/loader.py +90 -0
- sqlserver_semantic_mcp/policy/models.py +43 -0
- sqlserver_semantic_mcp/server/__init__.py +0 -0
- sqlserver_semantic_mcp/server/app.py +125 -0
- sqlserver_semantic_mcp/server/compact.py +74 -0
- sqlserver_semantic_mcp/server/prompts/__init__.py +5 -0
- sqlserver_semantic_mcp/server/prompts/analysis.py +56 -0
- sqlserver_semantic_mcp/server/prompts/discovery.py +55 -0
- sqlserver_semantic_mcp/server/prompts/execution.py +64 -0
- sqlserver_semantic_mcp/server/prompts/registry.py +41 -0
- sqlserver_semantic_mcp/server/resources/__init__.py +1 -0
- sqlserver_semantic_mcp/server/resources/schema.py +144 -0
- sqlserver_semantic_mcp/server/tools/__init__.py +42 -0
- sqlserver_semantic_mcp/server/tools/cache.py +24 -0
- sqlserver_semantic_mcp/server/tools/metadata.py +167 -0
- sqlserver_semantic_mcp/server/tools/metrics.py +44 -0
- sqlserver_semantic_mcp/server/tools/object_tool.py +113 -0
- sqlserver_semantic_mcp/server/tools/policy.py +48 -0
- sqlserver_semantic_mcp/server/tools/query.py +159 -0
- sqlserver_semantic_mcp/server/tools/relationship.py +104 -0
- sqlserver_semantic_mcp/server/tools/semantic.py +112 -0
- sqlserver_semantic_mcp/server/tools/shape.py +204 -0
- sqlserver_semantic_mcp/server/tools/workflow.py +307 -0
- sqlserver_semantic_mcp/services/__init__.py +0 -0
- sqlserver_semantic_mcp/services/metadata_service.py +173 -0
- sqlserver_semantic_mcp/services/metrics_service.py +124 -0
- sqlserver_semantic_mcp/services/object_service.py +187 -0
- sqlserver_semantic_mcp/services/policy_service.py +59 -0
- sqlserver_semantic_mcp/services/query_service.py +321 -0
- sqlserver_semantic_mcp/services/relationship_service.py +160 -0
- sqlserver_semantic_mcp/services/semantic_service.py +277 -0
- sqlserver_semantic_mcp/workflows/__init__.py +26 -0
- sqlserver_semantic_mcp/workflows/bundle.py +157 -0
- sqlserver_semantic_mcp/workflows/contracts.py +64 -0
- sqlserver_semantic_mcp/workflows/discovery_flow.py +116 -0
- sqlserver_semantic_mcp/workflows/facade.py +117 -0
- sqlserver_semantic_mcp/workflows/query_flow.py +120 -0
- sqlserver_semantic_mcp/workflows/recommendations.py +161 -0
- sqlserver_semantic_mcp/workflows/router.py +59 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/METADATA +679 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/RECORD +74 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/WHEEL +5 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/entry_points.txt +2 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/licenses/LICENSE +21 -0
- sqlserver_semantic_mcp-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from mcp.types import Tool
|
|
2
|
+
|
|
3
|
+
from ..app import get_context, register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register() -> None:
|
|
7
|
+
register_tool(
|
|
8
|
+
Tool(
|
|
9
|
+
name="get_execution_policy",
|
|
10
|
+
description="Return the active execution policy.",
|
|
11
|
+
inputSchema={"type": "object", "properties": {}},
|
|
12
|
+
),
|
|
13
|
+
_get_policy,
|
|
14
|
+
)
|
|
15
|
+
register_tool(
|
|
16
|
+
Tool(
|
|
17
|
+
name="validate_sql_against_policy",
|
|
18
|
+
description="Validate SQL against active policy WITHOUT executing.",
|
|
19
|
+
inputSchema={
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {"query": {"type": "string"}},
|
|
22
|
+
"required": ["query"],
|
|
23
|
+
},
|
|
24
|
+
),
|
|
25
|
+
_validate_sql,
|
|
26
|
+
)
|
|
27
|
+
register_tool(
|
|
28
|
+
Tool(
|
|
29
|
+
name="refresh_policy",
|
|
30
|
+
description="Reload policy file from disk.",
|
|
31
|
+
inputSchema={"type": "object", "properties": {}},
|
|
32
|
+
),
|
|
33
|
+
_refresh,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _get_policy(args: dict) -> dict:
|
|
38
|
+
return get_context().policy.current_policy().model_dump()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _validate_sql(args: dict) -> dict:
|
|
42
|
+
return get_context().policy.validate(args["query"])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _refresh(args: dict) -> dict:
|
|
46
|
+
ctx = get_context()
|
|
47
|
+
ctx.policy.reload()
|
|
48
|
+
return {"reloaded": True, "profile": ctx.policy.current_policy().profile_name}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from mcp.types import Tool
|
|
2
|
+
|
|
3
|
+
from ..app import get_context, register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_DETAIL_PROP = {
|
|
7
|
+
"type": "string", "enum": ["brief", "standard", "full"], "default": "brief",
|
|
8
|
+
}
|
|
9
|
+
_BUDGET_PROP = {
|
|
10
|
+
"type": "string", "enum": ["tiny", "low", "medium", "high"],
|
|
11
|
+
}
|
|
12
|
+
_RESPONSE_MODE_PROP = {
|
|
13
|
+
"type": "string", "enum": ["summary", "rows", "sample", "count_only"],
|
|
14
|
+
"description": "summary=columns+count; rows=full page; "
|
|
15
|
+
"sample=columns+first N; count_only=row_count only.",
|
|
16
|
+
}
|
|
17
|
+
_AFFECTED_POLICY_PROP = {
|
|
18
|
+
"type": "string", "enum": ["strict", "report"],
|
|
19
|
+
"description": "strict = roll back if affected rows exceed cap; "
|
|
20
|
+
"report = execute and report exceeded_cap.",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register() -> None:
|
|
25
|
+
register_tool(
|
|
26
|
+
Tool(
|
|
27
|
+
name="validate_query",
|
|
28
|
+
description=(
|
|
29
|
+
"Analyze a SQL query and report intent + whether policy allows "
|
|
30
|
+
"it. Use this when you want to test a query without executing."
|
|
31
|
+
),
|
|
32
|
+
inputSchema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {"query": {"type": "string"}},
|
|
35
|
+
"required": ["query"],
|
|
36
|
+
},
|
|
37
|
+
),
|
|
38
|
+
_validate,
|
|
39
|
+
)
|
|
40
|
+
register_tool(
|
|
41
|
+
Tool(
|
|
42
|
+
name="run_safe_query",
|
|
43
|
+
description=(
|
|
44
|
+
"Execute SQL after policy validation. Result rows are truncated "
|
|
45
|
+
"to max_rows_returned. Prefer plan_or_execute_query for the "
|
|
46
|
+
"shortest safe path when SQL is already known."
|
|
47
|
+
),
|
|
48
|
+
inputSchema={
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"query": {"type": "string"},
|
|
52
|
+
"max_rows": {"type": "integer", "minimum": 1},
|
|
53
|
+
},
|
|
54
|
+
"required": ["query"],
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
_run_safe,
|
|
58
|
+
)
|
|
59
|
+
register_tool(
|
|
60
|
+
Tool(
|
|
61
|
+
name="plan_or_execute_query",
|
|
62
|
+
description=(
|
|
63
|
+
"v0.5 main entry for SQL-ready agents. mode=auto validates then "
|
|
64
|
+
"executes if safe; mode=validate_only stops after validation; "
|
|
65
|
+
"mode=dry_run returns preview without side effects. Do not use "
|
|
66
|
+
"this for schema discovery — use discover_relevant_tables first "
|
|
67
|
+
"when the target tables are unknown."
|
|
68
|
+
),
|
|
69
|
+
inputSchema={
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"query": _required_query(),
|
|
73
|
+
"mode": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"enum": ["auto", "validate_only", "dry_run",
|
|
76
|
+
"execute_if_safe"],
|
|
77
|
+
"default": "auto",
|
|
78
|
+
},
|
|
79
|
+
"max_rows": {"type": "integer", "minimum": 1},
|
|
80
|
+
"return_mode": _RESPONSE_MODE_PROP,
|
|
81
|
+
"detail": _DETAIL_PROP,
|
|
82
|
+
"token_budget_hint": _BUDGET_PROP,
|
|
83
|
+
"affected_rows_policy": _AFFECTED_POLICY_PROP,
|
|
84
|
+
},
|
|
85
|
+
"required": ["query"],
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
_plan_or_execute,
|
|
89
|
+
)
|
|
90
|
+
register_tool(
|
|
91
|
+
Tool(
|
|
92
|
+
name="preview_safe_query",
|
|
93
|
+
description=(
|
|
94
|
+
"Return a minimal plan — operation, affected tables, policy "
|
|
95
|
+
"outcome, applied row caps — without executing."
|
|
96
|
+
),
|
|
97
|
+
inputSchema={
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"query": _required_query(),
|
|
101
|
+
"max_rows": {"type": "integer", "minimum": 1},
|
|
102
|
+
},
|
|
103
|
+
"required": ["query"],
|
|
104
|
+
},
|
|
105
|
+
),
|
|
106
|
+
_preview,
|
|
107
|
+
)
|
|
108
|
+
register_tool(
|
|
109
|
+
Tool(
|
|
110
|
+
name="estimate_execution_risk",
|
|
111
|
+
description=(
|
|
112
|
+
"Estimate payload / policy / qualification risks for a SQL "
|
|
113
|
+
"string without executing it."
|
|
114
|
+
),
|
|
115
|
+
inputSchema={
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {"query": {"type": "string"}},
|
|
118
|
+
"required": ["query"],
|
|
119
|
+
},
|
|
120
|
+
),
|
|
121
|
+
_estimate_risk,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _required_query() -> dict:
|
|
126
|
+
return {"type": "string", "description": "SQL to execute / validate."}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _validate(args: dict) -> dict:
|
|
130
|
+
return get_context().query.validate(args["query"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _run_safe(args: dict) -> dict:
|
|
134
|
+
return get_context().query.run_safe_query(
|
|
135
|
+
args["query"], max_rows=args.get("max_rows"),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _plan_or_execute(args: dict) -> dict:
|
|
140
|
+
ctx = get_context()
|
|
141
|
+
return ctx.workflow.plan_or_execute_query(
|
|
142
|
+
args["query"],
|
|
143
|
+
mode=args.get("mode", "auto"),
|
|
144
|
+
max_rows=args.get("max_rows"),
|
|
145
|
+
return_mode=args.get("return_mode"),
|
|
146
|
+
detail=args.get("detail", "brief"),
|
|
147
|
+
token_budget_hint=args.get("token_budget_hint"),
|
|
148
|
+
affected_rows_policy=args.get("affected_rows_policy"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def _preview(args: dict) -> dict:
|
|
153
|
+
return get_context().workflow.preview_safe_query(
|
|
154
|
+
args["query"], max_rows=args.get("max_rows"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def _estimate_risk(args: dict) -> dict:
|
|
159
|
+
return get_context().workflow.estimate_execution_risk(args["query"])
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from mcp.types import Tool
|
|
2
|
+
|
|
3
|
+
from ...services import relationship_service
|
|
4
|
+
from ..app import get_context, register_tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register() -> None:
|
|
8
|
+
register_tool(
|
|
9
|
+
Tool(
|
|
10
|
+
name="get_table_relationships",
|
|
11
|
+
description="List inbound + outbound FK relationships for a table.",
|
|
12
|
+
inputSchema={
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"schema": {"type": "string"},
|
|
16
|
+
"table": {"type": "string"},
|
|
17
|
+
},
|
|
18
|
+
"required": ["schema", "table"],
|
|
19
|
+
},
|
|
20
|
+
),
|
|
21
|
+
_rels,
|
|
22
|
+
)
|
|
23
|
+
register_tool(
|
|
24
|
+
Tool(
|
|
25
|
+
name="find_join_path",
|
|
26
|
+
description=(
|
|
27
|
+
"Find a shortest FK-based join path between two tables "
|
|
28
|
+
"(BFS, bidirectional edges). Use after candidate tables are "
|
|
29
|
+
"known. For ranking multiple reasonable paths, call "
|
|
30
|
+
"score_join_candidate next."
|
|
31
|
+
),
|
|
32
|
+
inputSchema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"from_schema": {"type": "string"},
|
|
36
|
+
"from_table": {"type": "string"},
|
|
37
|
+
"to_schema": {"type": "string"},
|
|
38
|
+
"to_table": {"type": "string"},
|
|
39
|
+
"max_hops": {"type": "integer", "minimum": 1, "default": 5},
|
|
40
|
+
},
|
|
41
|
+
"required": ["from_schema", "from_table", "to_schema", "to_table"],
|
|
42
|
+
},
|
|
43
|
+
),
|
|
44
|
+
_path,
|
|
45
|
+
)
|
|
46
|
+
register_tool(
|
|
47
|
+
Tool(
|
|
48
|
+
name="get_dependency_chain",
|
|
49
|
+
description=(
|
|
50
|
+
"List all tables reachable from a given table via FKs. "
|
|
51
|
+
"schemas param limits the BFS frontier to allowed schemas "
|
|
52
|
+
"(start table always included)."
|
|
53
|
+
),
|
|
54
|
+
inputSchema={
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"schema": {"type": "string"},
|
|
58
|
+
"table": {"type": "string"},
|
|
59
|
+
"max_depth": {"type": "integer", "default": 10},
|
|
60
|
+
"schemas": {"oneOf": [{"type": "string"},
|
|
61
|
+
{"type": "array",
|
|
62
|
+
"items": {"type": "string"}}]},
|
|
63
|
+
},
|
|
64
|
+
"required": ["schema", "table"],
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
_chain,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _rels(args: dict) -> list[dict]:
|
|
72
|
+
ctx = get_context()
|
|
73
|
+
return await relationship_service.get_table_relationships(
|
|
74
|
+
ctx.cfg.cache_path, ctx.cfg.mssql_database,
|
|
75
|
+
args["schema"], args["table"],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _path(args: dict) -> dict:
|
|
80
|
+
ctx = get_context()
|
|
81
|
+
path = await relationship_service.find_join_path(
|
|
82
|
+
ctx.cfg.cache_path, ctx.cfg.mssql_database,
|
|
83
|
+
args["from_schema"], args["from_table"],
|
|
84
|
+
args["to_schema"], args["to_table"],
|
|
85
|
+
max_hops=args.get("max_hops", 5),
|
|
86
|
+
)
|
|
87
|
+
return {"found": path is not None, "path": path or []}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _chain(args: dict) -> list[dict]:
|
|
91
|
+
ctx = get_context()
|
|
92
|
+
raw = args.get("schemas")
|
|
93
|
+
if isinstance(raw, str):
|
|
94
|
+
schemas = [raw] if raw else None
|
|
95
|
+
elif isinstance(raw, list):
|
|
96
|
+
schemas = [s for s in raw if isinstance(s, str) and s] or None
|
|
97
|
+
else:
|
|
98
|
+
schemas = None
|
|
99
|
+
return await relationship_service.get_dependency_chain(
|
|
100
|
+
ctx.cfg.cache_path, ctx.cfg.mssql_database,
|
|
101
|
+
args["schema"], args["table"],
|
|
102
|
+
max_depth=args.get("max_depth", 10),
|
|
103
|
+
schemas=schemas,
|
|
104
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from mcp.types import Tool
|
|
4
|
+
|
|
5
|
+
from ...services import semantic_service
|
|
6
|
+
from ..app import get_context, register_tool
|
|
7
|
+
from .shape import project_classify, resolve_detail
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DETAIL_PROP = {
|
|
11
|
+
"type": "string", "enum": ["brief", "standard", "full"],
|
|
12
|
+
"default": "brief",
|
|
13
|
+
"description": "brief = type+confidence only; standard/full include reasons.",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register() -> None:
|
|
18
|
+
register_tool(
|
|
19
|
+
Tool(
|
|
20
|
+
name="classify_table",
|
|
21
|
+
description="Classify a table (fact / dimension / lookup / bridge / audit).",
|
|
22
|
+
inputSchema={
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"schema": {"type": "string"},
|
|
26
|
+
"table": {"type": "string"},
|
|
27
|
+
"force": {"type": "boolean", "default": False},
|
|
28
|
+
"detail": _DETAIL_PROP,
|
|
29
|
+
},
|
|
30
|
+
"required": ["schema", "table"],
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
_classify,
|
|
34
|
+
)
|
|
35
|
+
register_tool(
|
|
36
|
+
Tool(
|
|
37
|
+
name="analyze_columns",
|
|
38
|
+
description="Return semantic labels for each column (audit, status, etc.).",
|
|
39
|
+
inputSchema={
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"schema": {"type": "string"},
|
|
43
|
+
"table": {"type": "string"},
|
|
44
|
+
},
|
|
45
|
+
"required": ["schema", "table"],
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
_columns,
|
|
49
|
+
)
|
|
50
|
+
register_tool(
|
|
51
|
+
Tool(
|
|
52
|
+
name="detect_lookup_tables",
|
|
53
|
+
description=(
|
|
54
|
+
"Scan DB and return likely lookup tables. Supports schema / "
|
|
55
|
+
"keyword / confidence_min filters to limit the sweep."
|
|
56
|
+
),
|
|
57
|
+
inputSchema={
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"schema": {"oneOf": [{"type": "string"},
|
|
61
|
+
{"type": "array",
|
|
62
|
+
"items": {"type": "string"}}]},
|
|
63
|
+
"keyword": {"type": "string"},
|
|
64
|
+
"confidence_min": {"type": "number",
|
|
65
|
+
"minimum": 0.0, "maximum": 1.0,
|
|
66
|
+
"default": 0.0},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
_lookups,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _normalize_schema_filter(raw) -> Optional[list[str]]:
|
|
75
|
+
if raw is None:
|
|
76
|
+
return None
|
|
77
|
+
if isinstance(raw, str):
|
|
78
|
+
return [raw] if raw else None
|
|
79
|
+
if isinstance(raw, list):
|
|
80
|
+
vals = [s for s in raw if isinstance(s, str) and s]
|
|
81
|
+
return vals or None
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _classify(args: dict) -> dict:
|
|
86
|
+
ctx = get_context()
|
|
87
|
+
detail = resolve_detail(args)
|
|
88
|
+
classification = await semantic_service.classify_table(
|
|
89
|
+
ctx.cfg.cache_path, ctx.cfg.mssql_database,
|
|
90
|
+
args["schema"], args["table"],
|
|
91
|
+
force=args.get("force", False),
|
|
92
|
+
)
|
|
93
|
+
return project_classify(classification, detail)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def _columns(args: dict) -> list[dict]:
|
|
97
|
+
ctx = get_context()
|
|
98
|
+
return await semantic_service.analyze_columns(
|
|
99
|
+
ctx.cfg.cache_path, ctx.cfg.mssql_database,
|
|
100
|
+
args["schema"], args["table"],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def _lookups(args: dict) -> list[dict]:
|
|
105
|
+
ctx = get_context()
|
|
106
|
+
schemas = _normalize_schema_filter(args.get("schema"))
|
|
107
|
+
keyword = args.get("keyword") or None
|
|
108
|
+
confidence_min = float(args.get("confidence_min", 0.0))
|
|
109
|
+
return await semantic_service.detect_lookup_tables(
|
|
110
|
+
ctx.cfg.cache_path, ctx.cfg.mssql_database,
|
|
111
|
+
schemas=schemas, keyword=keyword, confidence_min=confidence_min,
|
|
112
|
+
)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Detail-tier projection helpers for P1 response contract reset.
|
|
2
|
+
|
|
3
|
+
See docs/superpowers/specs/2026-04-19-p1-response-contract-reset-design.md.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
VALID_DETAILS: frozenset[str] = frozenset({"brief", "standard", "full"})
|
|
8
|
+
|
|
9
|
+
_IMPORTANT_COLS_CAP = 8
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetailError(ValueError):
|
|
13
|
+
"""Raised when an invalid `detail` value is passed."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_detail(args: dict) -> str:
|
|
17
|
+
val = args.get("detail", "brief")
|
|
18
|
+
if val not in VALID_DETAILS:
|
|
19
|
+
raise DetailError(
|
|
20
|
+
f"invalid detail '{val}'; expected one of {sorted(VALID_DETAILS)}"
|
|
21
|
+
)
|
|
22
|
+
return val
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _important_columns(
|
|
26
|
+
columns: list[dict], pk: list[str], fks: list[dict],
|
|
27
|
+
semantic_map: dict[str, str],
|
|
28
|
+
) -> list[str]:
|
|
29
|
+
seen: set[str] = set()
|
|
30
|
+
order: list[str] = []
|
|
31
|
+
|
|
32
|
+
def push(name: str) -> None:
|
|
33
|
+
if name in seen or name is None:
|
|
34
|
+
return
|
|
35
|
+
seen.add(name)
|
|
36
|
+
order.append(name)
|
|
37
|
+
if len(order) >= _IMPORTANT_COLS_CAP:
|
|
38
|
+
raise StopIteration
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
for name in pk:
|
|
42
|
+
push(name)
|
|
43
|
+
for fk in fks:
|
|
44
|
+
push(fk.get("column_name"))
|
|
45
|
+
# columns with a non-generic semantic tag, in ordinal order
|
|
46
|
+
for col in columns:
|
|
47
|
+
name = col["column_name"]
|
|
48
|
+
sem = semantic_map.get(name)
|
|
49
|
+
if sem and sem != "generic":
|
|
50
|
+
push(name)
|
|
51
|
+
# fill remaining with next columns in ordinal order
|
|
52
|
+
for col in columns:
|
|
53
|
+
push(col["column_name"])
|
|
54
|
+
except StopIteration:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
return order[:_IMPORTANT_COLS_CAP]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def project_describe_table(
|
|
61
|
+
full: dict, detail: str,
|
|
62
|
+
classification: Optional[dict],
|
|
63
|
+
column_semantics: dict[str, str],
|
|
64
|
+
) -> dict:
|
|
65
|
+
schema = full.get("schema_name", "")
|
|
66
|
+
table = full.get("table_name", "")
|
|
67
|
+
columns = full.get("columns", [])
|
|
68
|
+
pk = full.get("primary_key", []) or []
|
|
69
|
+
fks = full.get("foreign_keys", []) or []
|
|
70
|
+
cls_type = (classification or {}).get("type", "unknown")
|
|
71
|
+
|
|
72
|
+
fk_to = sorted({
|
|
73
|
+
f"{fk.get('ref_schema')}.{fk.get('ref_table')}"
|
|
74
|
+
for fk in fks
|
|
75
|
+
if fk.get("ref_schema") and fk.get("ref_table")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
brief: dict[str, Any] = {
|
|
79
|
+
"table": f"{schema}.{table}",
|
|
80
|
+
"column_count": len(columns),
|
|
81
|
+
"pk": list(pk),
|
|
82
|
+
"fk_to": fk_to,
|
|
83
|
+
"important_columns": _important_columns(columns, pk, fks, column_semantics),
|
|
84
|
+
"classification": cls_type,
|
|
85
|
+
}
|
|
86
|
+
if detail == "brief":
|
|
87
|
+
return brief
|
|
88
|
+
|
|
89
|
+
# standard: brief + full columns (name/type/nullable) + full FK rows
|
|
90
|
+
standard_cols = [
|
|
91
|
+
{"name": c["column_name"], "type": c.get("data_type"),
|
|
92
|
+
"is_nullable": bool(c.get("is_nullable"))}
|
|
93
|
+
for c in columns
|
|
94
|
+
]
|
|
95
|
+
standard: dict[str, Any] = {
|
|
96
|
+
**brief,
|
|
97
|
+
"columns": standard_cols,
|
|
98
|
+
"foreign_keys": list(fks),
|
|
99
|
+
}
|
|
100
|
+
if detail == "standard":
|
|
101
|
+
return standard
|
|
102
|
+
|
|
103
|
+
# full: standard + indexes + description + per-column default_value + description
|
|
104
|
+
full_cols = []
|
|
105
|
+
for c in columns:
|
|
106
|
+
full_cols.append({
|
|
107
|
+
"name": c["column_name"],
|
|
108
|
+
"type": c.get("data_type"),
|
|
109
|
+
"is_nullable": bool(c.get("is_nullable")),
|
|
110
|
+
"max_length": c.get("max_length"),
|
|
111
|
+
"default_value": c.get("default_value"),
|
|
112
|
+
"description": c.get("description"),
|
|
113
|
+
})
|
|
114
|
+
return {
|
|
115
|
+
**brief,
|
|
116
|
+
"columns": full_cols,
|
|
117
|
+
"foreign_keys": list(fks),
|
|
118
|
+
"indexes": full.get("indexes", []),
|
|
119
|
+
"description": full.get("description"),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def project_get_columns(
|
|
124
|
+
columns: list[dict], detail: str,
|
|
125
|
+
semantic_map: dict[str, str],
|
|
126
|
+
) -> list[dict]:
|
|
127
|
+
def semantic_for(name: str) -> str:
|
|
128
|
+
return semantic_map.get(name) or "generic"
|
|
129
|
+
|
|
130
|
+
if detail == "brief":
|
|
131
|
+
return [
|
|
132
|
+
{"name": c["column_name"], "semantic": semantic_for(c["column_name"])}
|
|
133
|
+
for c in columns
|
|
134
|
+
]
|
|
135
|
+
if detail == "standard":
|
|
136
|
+
return [
|
|
137
|
+
{"name": c["column_name"], "type": c.get("data_type"),
|
|
138
|
+
"is_nullable": bool(c.get("is_nullable")),
|
|
139
|
+
"semantic": semantic_for(c["column_name"])}
|
|
140
|
+
for c in columns
|
|
141
|
+
]
|
|
142
|
+
# full
|
|
143
|
+
return [
|
|
144
|
+
{"name": c["column_name"], "type": c.get("data_type"),
|
|
145
|
+
"max_length": c.get("max_length"),
|
|
146
|
+
"is_nullable": bool(c.get("is_nullable")),
|
|
147
|
+
"default_value": c.get("default_value"),
|
|
148
|
+
"description": c.get("description"),
|
|
149
|
+
"semantic": semantic_for(c["column_name"])}
|
|
150
|
+
for c in columns
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def project_classify(classification: dict, detail: str) -> dict:
|
|
155
|
+
if detail == "brief":
|
|
156
|
+
return {
|
|
157
|
+
"type": classification.get("type"),
|
|
158
|
+
"confidence": classification.get("confidence"),
|
|
159
|
+
}
|
|
160
|
+
return dict(classification)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def project_describe_object(
|
|
164
|
+
obj: dict, detail: str, include_definition: bool,
|
|
165
|
+
) -> dict:
|
|
166
|
+
schema = obj.get("schema", "")
|
|
167
|
+
name = obj.get("object_name", "")
|
|
168
|
+
obj_type = obj.get("object_type") or obj.get("type")
|
|
169
|
+
|
|
170
|
+
brief: dict[str, Any] = {
|
|
171
|
+
"object": f"{schema}.{name}",
|
|
172
|
+
"type": obj_type,
|
|
173
|
+
"depends_on": list(obj.get("dependencies", []) or []),
|
|
174
|
+
"definition_bytes": obj.get("definition_bytes"),
|
|
175
|
+
}
|
|
176
|
+
if obj.get("status") == "error":
|
|
177
|
+
brief["status"] = "error"
|
|
178
|
+
brief["error_message"] = obj.get("error_message")
|
|
179
|
+
|
|
180
|
+
# include_definition explicit true overrides brief
|
|
181
|
+
if detail == "brief":
|
|
182
|
+
if include_definition and obj.get("definition"):
|
|
183
|
+
brief["definition"] = obj["definition"]
|
|
184
|
+
brief["definition_hash"] = obj.get("definition_hash")
|
|
185
|
+
return brief
|
|
186
|
+
|
|
187
|
+
# standard / full share more fields
|
|
188
|
+
standard: dict[str, Any] = {
|
|
189
|
+
**brief,
|
|
190
|
+
"definition_hash": obj.get("definition_hash"),
|
|
191
|
+
"read_tables": list(obj.get("read_tables", []) or []),
|
|
192
|
+
"write_tables": list(obj.get("write_tables", []) or []),
|
|
193
|
+
"affected_tables": list(obj.get("affected_tables", []) or []),
|
|
194
|
+
"description": obj.get("description"),
|
|
195
|
+
"status": obj.get("status"),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if detail == "standard":
|
|
199
|
+
if include_definition and obj.get("definition"):
|
|
200
|
+
standard["definition"] = obj["definition"]
|
|
201
|
+
return standard
|
|
202
|
+
|
|
203
|
+
# full: always include definition
|
|
204
|
+
return {**standard, "definition": obj.get("definition")}
|