tdqs 0.1.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.
- tdqs/__init__.py +153 -0
- tdqs/__main__.py +3 -0
- tdqs/build_server_coherence_prompt.py +63 -0
- tdqs/build_tool_scoring_prompt.py +61 -0
- tdqs/camel_model.py +17 -0
- tdqs/cli/__init__.py +1 -0
- tdqs/cli/cli_usage.py +31 -0
- tdqs/cli/format_lint_report_text.py +41 -0
- tdqs/cli/format_score_report_text.py +41 -0
- tdqs/cli/main.py +10 -0
- tdqs/cli/parse_cli_arguments.py +300 -0
- tdqs/cli/resolve_tool_definitions.py +43 -0
- tdqs/cli/run_cli.py +202 -0
- tdqs/cli/split_command_line.py +67 -0
- tdqs/compute_context_signals.py +85 -0
- tdqs/compute_input_hash.py +10 -0
- tdqs/compute_invocation_cost.py +14 -0
- tdqs/compute_smells.py +8 -0
- tdqs/compute_tdqs.py +16 -0
- tdqs/compute_tier.py +11 -0
- tdqs/context_signals.py +34 -0
- tdqs/create_llm_client.py +254 -0
- tdqs/describe_validation_error.py +12 -0
- tdqs/evaluate_hard_gates.py +33 -0
- tdqs/find_shadow_candidates.py +80 -0
- tdqs/format_lint_report_markdown.py +76 -0
- tdqs/format_score_report_markdown.py +90 -0
- tdqs/generated_at.py +9 -0
- tdqs/lint_report.py +40 -0
- tdqs/lint_rules.py +83 -0
- tdqs/lint_server.py +165 -0
- tdqs/llm_client.py +17 -0
- tdqs/load_tool_definitions.py +128 -0
- tdqs/package_version.py +8 -0
- tdqs/parse_tool_definitions.py +82 -0
- tdqs/prompts/__init__.py +1 -0
- tdqs/prompts/server_coherence_system_prompt.py +79 -0
- tdqs/prompts/tool_scoring_system_prompt.py +91 -0
- tdqs/py.typed +0 -0
- tdqs/request_hosted_report.py +104 -0
- tdqs/rollup_server_score.py +61 -0
- tdqs/round1.py +11 -0
- tdqs/round_half_up.py +7 -0
- tdqs/sanitize_for_prompt.py +17 -0
- tdqs/score_report.py +20 -0
- tdqs/score_server.py +75 -0
- tdqs/score_server_coherence.py +97 -0
- tdqs/score_tool_definition.py +118 -0
- tdqs/serialize_tool_definition.py +21 -0
- tdqs/server_score.py +53 -0
- tdqs/spec/__init__.py +1 -0
- tdqs/spec/coherence_dimensions.py +96 -0
- tdqs/spec/flags.py +84 -0
- tdqs/spec/spec_version.py +7 -0
- tdqs/spec/tiers.py +25 -0
- tdqs/spec/tool_dimensions.py +141 -0
- tdqs/stringify_json.py +179 -0
- tdqs/tdqs_input_error.py +4 -0
- tdqs/tdqs_llm_error.py +17 -0
- tdqs/tool_definition.py +32 -0
- tdqs/tool_definitions_source.py +28 -0
- tdqs/tool_score.py +49 -0
- tdqs/traverse_required_subtree.py +212 -0
- tdqs/trim_whitespace.py +13 -0
- tdqs/utf16_length.py +5 -0
- tdqs-0.1.0.dist-info/METADATA +134 -0
- tdqs-0.1.0.dist-info/RECORD +69 -0
- tdqs-0.1.0.dist-info/WHEEL +4 -0
- tdqs-0.1.0.dist-info/entry_points.txt +3 -0
tdqs/__init__.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""The reference implementation of the Tool Definition Quality Score, for Python.
|
|
2
|
+
|
|
3
|
+
The package's public surface. The modules are one export each; this is the one stable entry
|
|
4
|
+
point, so `from tdqs import score_server` keeps working when a module moves.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tdqs.build_server_coherence_prompt import CoherencePromptTool, build_server_coherence_prompt
|
|
8
|
+
from tdqs.build_tool_scoring_prompt import build_tool_scoring_prompt
|
|
9
|
+
from tdqs.compute_context_signals import compute_context_signals
|
|
10
|
+
from tdqs.compute_input_hash import compute_input_hash
|
|
11
|
+
from tdqs.compute_invocation_cost import compute_invocation_cost
|
|
12
|
+
from tdqs.compute_smells import compute_smells
|
|
13
|
+
from tdqs.compute_tdqs import compute_tdqs
|
|
14
|
+
from tdqs.compute_tier import compute_tier
|
|
15
|
+
from tdqs.context_signals import AnnotationValues, ContextSignals
|
|
16
|
+
from tdqs.create_llm_client import OpenAiCompatibleLlmClient, create_llm_client
|
|
17
|
+
from tdqs.evaluate_hard_gates import HardGateResult, evaluate_hard_gates
|
|
18
|
+
from tdqs.find_shadow_candidates import CostedTool, ShadowCandidate, find_shadow_candidates
|
|
19
|
+
from tdqs.format_lint_report_markdown import format_lint_report_markdown
|
|
20
|
+
from tdqs.format_score_report_markdown import format_score_report_markdown
|
|
21
|
+
from tdqs.lint_report import LintedTool, LintFinding, LintReport, ServerSummary
|
|
22
|
+
from tdqs.lint_rules import LINT_RULES, LintRule, LintRuleKey, LintSeverity
|
|
23
|
+
from tdqs.lint_server import lint_server
|
|
24
|
+
from tdqs.llm_client import LlmClient
|
|
25
|
+
from tdqs.load_tool_definitions import load_tool_definitions
|
|
26
|
+
from tdqs.package_version import PACKAGE_VERSION
|
|
27
|
+
from tdqs.parse_tool_definitions import ParsedToolDefinitions, parse_tool_definitions
|
|
28
|
+
from tdqs.prompts.server_coherence_system_prompt import SERVER_COHERENCE_SYSTEM_PROMPT
|
|
29
|
+
from tdqs.prompts.tool_scoring_system_prompt import TOOL_SCORING_SYSTEM_PROMPT
|
|
30
|
+
from tdqs.request_hosted_report import HostedReport, HostedReportStatus, request_hosted_report
|
|
31
|
+
from tdqs.rollup_server_score import rollup_server_score
|
|
32
|
+
from tdqs.round1 import round1
|
|
33
|
+
from tdqs.score_report import ScoreReport
|
|
34
|
+
from tdqs.score_server import score_server
|
|
35
|
+
from tdqs.score_server_coherence import ServerCoherence, score_server_coherence
|
|
36
|
+
from tdqs.score_tool_definition import score_tool_definition
|
|
37
|
+
from tdqs.serialize_tool_definition import serialize_tool_definition
|
|
38
|
+
from tdqs.server_score import (
|
|
39
|
+
CoherenceJustifications,
|
|
40
|
+
CoherenceScores,
|
|
41
|
+
ServerScore,
|
|
42
|
+
ShadowingRisk,
|
|
43
|
+
)
|
|
44
|
+
from tdqs.spec.coherence_dimensions import (
|
|
45
|
+
COHERENCE_DIMENSIONS,
|
|
46
|
+
CoherenceDimension,
|
|
47
|
+
CoherenceDimensionKey,
|
|
48
|
+
)
|
|
49
|
+
from tdqs.spec.flags import FLAGS, Flag, HardGateFlag, ServerFlag, ToolFlag
|
|
50
|
+
from tdqs.spec.spec_version import SPEC_VERSION
|
|
51
|
+
from tdqs.spec.tiers import TIERS, Tier, TierDefinition
|
|
52
|
+
from tdqs.spec.tool_dimensions import (
|
|
53
|
+
TOOL_DIMENSIONS,
|
|
54
|
+
DimensionScore,
|
|
55
|
+
ToolDimension,
|
|
56
|
+
ToolDimensionKey,
|
|
57
|
+
)
|
|
58
|
+
from tdqs.stringify_json import stringify_json
|
|
59
|
+
from tdqs.tdqs_input_error import TdqsInputError
|
|
60
|
+
from tdqs.tdqs_llm_error import TdqsLlmError
|
|
61
|
+
from tdqs.tool_definition import ToolDefinition
|
|
62
|
+
from tdqs.tool_definitions_source import (
|
|
63
|
+
CommandSource,
|
|
64
|
+
FileSource,
|
|
65
|
+
ToolDefinitionsSource,
|
|
66
|
+
UrlSource,
|
|
67
|
+
)
|
|
68
|
+
from tdqs.tool_score import DimensionJustifications, DimensionScores, Justification, ToolScore
|
|
69
|
+
from tdqs.traverse_required_subtree import RequiredSubtreeSignals, traverse_required_subtree
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"COHERENCE_DIMENSIONS",
|
|
73
|
+
"FLAGS",
|
|
74
|
+
"LINT_RULES",
|
|
75
|
+
"PACKAGE_VERSION",
|
|
76
|
+
"SERVER_COHERENCE_SYSTEM_PROMPT",
|
|
77
|
+
"SPEC_VERSION",
|
|
78
|
+
"TIERS",
|
|
79
|
+
"TOOL_DIMENSIONS",
|
|
80
|
+
"TOOL_SCORING_SYSTEM_PROMPT",
|
|
81
|
+
"AnnotationValues",
|
|
82
|
+
"CoherenceDimension",
|
|
83
|
+
"CoherenceDimensionKey",
|
|
84
|
+
"CoherenceJustifications",
|
|
85
|
+
"CoherencePromptTool",
|
|
86
|
+
"CoherenceScores",
|
|
87
|
+
"CommandSource",
|
|
88
|
+
"ContextSignals",
|
|
89
|
+
"CostedTool",
|
|
90
|
+
"DimensionJustifications",
|
|
91
|
+
"DimensionScore",
|
|
92
|
+
"DimensionScores",
|
|
93
|
+
"FileSource",
|
|
94
|
+
"Flag",
|
|
95
|
+
"HardGateFlag",
|
|
96
|
+
"HardGateResult",
|
|
97
|
+
"HostedReport",
|
|
98
|
+
"HostedReportStatus",
|
|
99
|
+
"Justification",
|
|
100
|
+
"LintFinding",
|
|
101
|
+
"LintReport",
|
|
102
|
+
"LintRule",
|
|
103
|
+
"LintRuleKey",
|
|
104
|
+
"LintSeverity",
|
|
105
|
+
"LintedTool",
|
|
106
|
+
"LlmClient",
|
|
107
|
+
"OpenAiCompatibleLlmClient",
|
|
108
|
+
"ParsedToolDefinitions",
|
|
109
|
+
"RequiredSubtreeSignals",
|
|
110
|
+
"ScoreReport",
|
|
111
|
+
"ServerCoherence",
|
|
112
|
+
"ServerFlag",
|
|
113
|
+
"ServerScore",
|
|
114
|
+
"ServerSummary",
|
|
115
|
+
"ShadowCandidate",
|
|
116
|
+
"ShadowingRisk",
|
|
117
|
+
"TdqsInputError",
|
|
118
|
+
"TdqsLlmError",
|
|
119
|
+
"Tier",
|
|
120
|
+
"TierDefinition",
|
|
121
|
+
"ToolDefinition",
|
|
122
|
+
"ToolDefinitionsSource",
|
|
123
|
+
"ToolDimension",
|
|
124
|
+
"ToolDimensionKey",
|
|
125
|
+
"ToolFlag",
|
|
126
|
+
"ToolScore",
|
|
127
|
+
"UrlSource",
|
|
128
|
+
"build_server_coherence_prompt",
|
|
129
|
+
"build_tool_scoring_prompt",
|
|
130
|
+
"compute_context_signals",
|
|
131
|
+
"compute_input_hash",
|
|
132
|
+
"compute_invocation_cost",
|
|
133
|
+
"compute_smells",
|
|
134
|
+
"compute_tdqs",
|
|
135
|
+
"compute_tier",
|
|
136
|
+
"create_llm_client",
|
|
137
|
+
"evaluate_hard_gates",
|
|
138
|
+
"find_shadow_candidates",
|
|
139
|
+
"format_lint_report_markdown",
|
|
140
|
+
"format_score_report_markdown",
|
|
141
|
+
"lint_server",
|
|
142
|
+
"load_tool_definitions",
|
|
143
|
+
"parse_tool_definitions",
|
|
144
|
+
"request_hosted_report",
|
|
145
|
+
"rollup_server_score",
|
|
146
|
+
"round1",
|
|
147
|
+
"score_server",
|
|
148
|
+
"score_server_coherence",
|
|
149
|
+
"score_tool_definition",
|
|
150
|
+
"serialize_tool_definition",
|
|
151
|
+
"stringify_json",
|
|
152
|
+
"traverse_required_subtree",
|
|
153
|
+
]
|
tdqs/__main__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from tdqs.context_signals import ContextSignals
|
|
5
|
+
from tdqs.find_shadow_candidates import ShadowCandidate
|
|
6
|
+
from tdqs.sanitize_for_prompt import sanitize_for_prompt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CoherencePromptTool:
|
|
11
|
+
context_signals: ContextSignals
|
|
12
|
+
description: str | None
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_server_coherence_prompt(
|
|
17
|
+
*,
|
|
18
|
+
server_name: str,
|
|
19
|
+
shadow_candidates: Sequence[ShadowCandidate],
|
|
20
|
+
tools: Sequence[CoherencePromptTool],
|
|
21
|
+
) -> str:
|
|
22
|
+
"""The user message of the coherence call, in the specification's template: every tool with
|
|
23
|
+
its invocation cost and the three signals behind it, then the candidate pairs the prefilter
|
|
24
|
+
emitted. The model is told to judge only those pairs, which is why they are spelled out rather
|
|
25
|
+
than left for it to find."""
|
|
26
|
+
tool_lines: list[str] = []
|
|
27
|
+
|
|
28
|
+
for tool in tools:
|
|
29
|
+
signals = tool.context_signals
|
|
30
|
+
cost = (
|
|
31
|
+
f"cost {signals.invocation_cost}: {signals.required_field_count} required, "
|
|
32
|
+
f"depth {signals.schema_depth}, {signals.union_choice_count} union choices"
|
|
33
|
+
)
|
|
34
|
+
text = (
|
|
35
|
+
"(no description)"
|
|
36
|
+
if tool.description is None
|
|
37
|
+
else str(sanitize_for_prompt(tool.description))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
tool_lines.append(f"- {tool.name} [{cost}]: {text}")
|
|
41
|
+
|
|
42
|
+
candidate_lines = [
|
|
43
|
+
f"{candidate.tool} (cost {candidate.invocation_cost}) may be shadowed by "
|
|
44
|
+
f"{candidate.cheaper_sibling} (cost {candidate.cheaper_sibling_invocation_cost})"
|
|
45
|
+
for candidate in shadow_candidates
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
return "\n".join(
|
|
49
|
+
[
|
|
50
|
+
f"SERVER NAME: {sanitize_for_prompt(server_name)}",
|
|
51
|
+
f"TOOL COUNT: {len(tools)}",
|
|
52
|
+
"",
|
|
53
|
+
"<tools>",
|
|
54
|
+
*tool_lines,
|
|
55
|
+
"</tools>",
|
|
56
|
+
"",
|
|
57
|
+
"<shadow-candidates>",
|
|
58
|
+
"\n".join(candidate_lines) if len(candidate_lines) > 0 else "None",
|
|
59
|
+
"</shadow-candidates>",
|
|
60
|
+
"",
|
|
61
|
+
"Respond with JSON only.",
|
|
62
|
+
]
|
|
63
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from tdqs.context_signals import ContextSignals
|
|
4
|
+
from tdqs.sanitize_for_prompt import sanitize_for_prompt
|
|
5
|
+
from tdqs.stringify_json import stringify_json
|
|
6
|
+
from tdqs.tool_definition import ToolDefinition
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _js_boolean(value: bool) -> str:
|
|
10
|
+
return "true" if value else "false"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_tool_scoring_prompt(
|
|
14
|
+
*, context_signals: ContextSignals, sibling_tool_names: Sequence[str], tool: ToolDefinition
|
|
15
|
+
) -> str:
|
|
16
|
+
"""The user message of the tool-scoring call, in the specification's template. Schemas are
|
|
17
|
+
pretty-printed in their own key order — the deterministic serialization is for hashing, and
|
|
18
|
+
the model reads the schema the way its author wrote it. The invocation-cost signals are
|
|
19
|
+
deliberately absent: the specification withholds them from this prompt."""
|
|
20
|
+
input_schema = stringify_json(
|
|
21
|
+
sanitize_for_prompt(tool.input_schema if tool.input_schema is not None else {}), indent=2
|
|
22
|
+
)
|
|
23
|
+
annotations = (
|
|
24
|
+
"None provided"
|
|
25
|
+
if tool.annotations is None
|
|
26
|
+
else stringify_json(sanitize_for_prompt(tool.annotations), indent=2)
|
|
27
|
+
)
|
|
28
|
+
siblings = "\n".join(sibling_tool_names) if len(sibling_tool_names) > 0 else "None"
|
|
29
|
+
description = sanitize_for_prompt(tool.description if tool.description is not None else "")
|
|
30
|
+
|
|
31
|
+
return "\n".join(
|
|
32
|
+
[
|
|
33
|
+
f"TOOL NAME: {tool.name}",
|
|
34
|
+
f"TITLE: {tool.title if tool.title is not None else 'null'}",
|
|
35
|
+
"",
|
|
36
|
+
"DESCRIPTION:",
|
|
37
|
+
f'"{description}"',
|
|
38
|
+
"",
|
|
39
|
+
"<input-schema>",
|
|
40
|
+
input_schema,
|
|
41
|
+
"</input-schema>",
|
|
42
|
+
"",
|
|
43
|
+
"<annotations>",
|
|
44
|
+
annotations,
|
|
45
|
+
"</annotations>",
|
|
46
|
+
"",
|
|
47
|
+
"CONTEXT SIGNALS:",
|
|
48
|
+
f"- Parameter count: {context_signals.param_count}",
|
|
49
|
+
f"- Required parameters: {context_signals.required_param_count}",
|
|
50
|
+
f"- Schema description coverage: {context_signals.schema_description_coverage}%",
|
|
51
|
+
f"- Parameters with enums: {context_signals.params_with_enums}",
|
|
52
|
+
f"- Has output schema: {_js_boolean(context_signals.has_output_schema)}",
|
|
53
|
+
f"- Has nested objects: {_js_boolean(context_signals.has_nested_objects)}",
|
|
54
|
+
"",
|
|
55
|
+
"<sibling-tools>",
|
|
56
|
+
siblings,
|
|
57
|
+
"</sibling-tools>",
|
|
58
|
+
"",
|
|
59
|
+
"Respond with JSON only.",
|
|
60
|
+
]
|
|
61
|
+
)
|
tdqs/camel_model.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
from pydantic.alias_generators import to_camel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CamelModel(BaseModel):
|
|
6
|
+
"""A report model whose JSON is the specification's: camelCase keys, in the field order the
|
|
7
|
+
class declares. Attributes stay snake_case, and both spellings are accepted on the way in, so
|
|
8
|
+
a report read back from `tdqs score --format json` and one built in Python validate alike.
|
|
9
|
+
Dimension-keyed maps are `TypedDict`s rather than models because their keys are snake_case in
|
|
10
|
+
the specification and must not be renamed."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(
|
|
13
|
+
alias_generator=to_camel,
|
|
14
|
+
serialize_by_alias=True,
|
|
15
|
+
validate_by_alias=True,
|
|
16
|
+
validate_by_name=True,
|
|
17
|
+
)
|
tdqs/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The `tdqs` command: argument parsing, the two subcommands and their terminal renderings."""
|
tdqs/cli/cli_usage.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
CLI_USAGE = """tdqs — Tool Definition Quality Score
|
|
2
|
+
|
|
3
|
+
Usage
|
|
4
|
+
tdqs lint [input] [--server-name <name>] [--format json|markdown|text] [--fail-on error|warning|never] [--output <path>]
|
|
5
|
+
tdqs score [input] [--model <id>] [--base-url <url>] [--api-key <key>] [--concurrency <n>]
|
|
6
|
+
[--request-overrides <json>] [--format json|markdown|text] [--fail-under A|B|C|D]
|
|
7
|
+
[--output <path>] [--hosted <site url>]
|
|
8
|
+
tdqs --version
|
|
9
|
+
tdqs --help
|
|
10
|
+
|
|
11
|
+
Input (exactly one)
|
|
12
|
+
--file <path> A JSON file: a tools/list result ({ "tools": [...] }), an array of tools, or one tool.
|
|
13
|
+
"-" reads standard input.
|
|
14
|
+
--command "<cmd>" Spawn a stdio MCP server and read its tools/list.
|
|
15
|
+
--url <url> Connect to a Streamable HTTP MCP server. Repeat --header "Name: value" for auth.
|
|
16
|
+
|
|
17
|
+
lint The deterministic stages only: context signals, hard gates, the shadow prefilter and the
|
|
18
|
+
checklist the specification ranks highest. Needs no model and no key.
|
|
19
|
+
score The full pipeline: every tool through the rubric, then coherence, then the server rollup.
|
|
20
|
+
Needs an OpenAI-compatible endpoint (--base-url, --api-key, --model, or the TDQS_BASE_URL,
|
|
21
|
+
TDQS_API_KEY and TDQS_MODEL environment variables), or --hosted to score through a TDQS site
|
|
22
|
+
with that site's API key as --api-key or TDQS_API_KEY.
|
|
23
|
+
--request-overrides (or TDQS_REQUEST_OVERRIDES) merges provider-specific JSON into every
|
|
24
|
+
request; turn extended reasoning off with it, e.g. '{"reasoning":{"enabled":false}}' on
|
|
25
|
+
OpenRouter — the calibration examples reproduce with reasoning off, at seconds per call.
|
|
26
|
+
|
|
27
|
+
Exit codes
|
|
28
|
+
0 done
|
|
29
|
+
1 the threshold was not met (--fail-on for lint, --fail-under for score)
|
|
30
|
+
2 usage error, unreadable input, unreachable server, or a model failure
|
|
31
|
+
"""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from tdqs.lint_report import LintReport
|
|
2
|
+
from tdqs.lint_rules import LintSeverity
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _yes_no(value: bool) -> str:
|
|
6
|
+
return "yes" if value else "no"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_lint_report_text(report: LintReport) -> str:
|
|
10
|
+
"""The terminal rendering: one line per finding, grouped by tool, terse enough to read in a CI
|
|
11
|
+
log. The markdown and JSON formats carry everything; this one carries what a person scans."""
|
|
12
|
+
lines: list[str] = [
|
|
13
|
+
(
|
|
14
|
+
f"TDQS lint · {report.server.name} · {report.server.tool_count} tools · specification "
|
|
15
|
+
f"{report.spec_version}"
|
|
16
|
+
),
|
|
17
|
+
"",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
for tool in report.tools:
|
|
21
|
+
signals = tool.context_signals
|
|
22
|
+
|
|
23
|
+
lines.append(
|
|
24
|
+
f"{tool.name} params {signals.param_count} · coverage "
|
|
25
|
+
f"{signals.schema_description_coverage}% · annotations "
|
|
26
|
+
f"{_yes_no(signals.has_annotations)} · output schema "
|
|
27
|
+
f"{_yes_no(signals.has_output_schema)} · cost {signals.invocation_cost}"
|
|
28
|
+
)
|
|
29
|
+
lines.extend(
|
|
30
|
+
f" {finding.severity.ljust(7)} {finding.rule}: {finding.message}"
|
|
31
|
+
for finding in tool.findings
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def count(severity: LintSeverity) -> int:
|
|
35
|
+
return sum(1 for finding in report.findings if finding.severity == severity)
|
|
36
|
+
|
|
37
|
+
lines.extend(
|
|
38
|
+
["", f"{count('error')} errors, {count('warning')} warnings, {count('info')} notes"]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from tdqs.score_report import ScoreReport
|
|
2
|
+
from tdqs.spec.tool_dimensions import TOOL_DIMENSIONS
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def format_score_report_text(report: ScoreReport) -> str:
|
|
6
|
+
"""The terminal rendering of a score: the rollup, then one line per tool with its six
|
|
7
|
+
dimension scores in rubric order. Justifications live in the markdown and JSON formats."""
|
|
8
|
+
server_score = report.server_score
|
|
9
|
+
model = "" if report.model is None else f" · model {report.model}"
|
|
10
|
+
headings = " ".join(dimension.key.split("_")[0][:8].ljust(8) for dimension in TOOL_DIMENSIONS)
|
|
11
|
+
lines: list[str] = [
|
|
12
|
+
(
|
|
13
|
+
f"TDQS {server_score.overall_tier} {server_score.overall_score:.1f} · "
|
|
14
|
+
f"{report.server.name} · {report.server.tool_count} tools"
|
|
15
|
+
),
|
|
16
|
+
(
|
|
17
|
+
f" tool definition quality {server_score.description_quality_tier} "
|
|
18
|
+
f"{server_score.description_quality_score:.1f} (mean {server_score.mean_tdqs:.1f}, "
|
|
19
|
+
f"min {server_score.min_tdqs:.1f}) · coherence {server_score.coherence_tier} "
|
|
20
|
+
f"{server_score.coherence_score:.1f}"
|
|
21
|
+
),
|
|
22
|
+
f" specification {report.spec_version}{model}",
|
|
23
|
+
"",
|
|
24
|
+
f" {'tool'.ljust(40)} tdqs {headings} flags",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
for tool in report.tools:
|
|
28
|
+
scores = " ".join(str(tool.scores[dimension.key]).ljust(8) for dimension in TOOL_DIMENSIONS)
|
|
29
|
+
flags = ", ".join([*tool.flags, *tool.server_flags])
|
|
30
|
+
|
|
31
|
+
lines.append(f"{tool.tier} {tool.name[:40].ljust(40)} {tool.tdqs:.1f} {scores} {flags}")
|
|
32
|
+
|
|
33
|
+
for risk in server_score.shadowing_risks:
|
|
34
|
+
lines.append("")
|
|
35
|
+
lines.append(
|
|
36
|
+
f"shadowing risk: {risk.tool} (cost {risk.invocation_cost}) may be shadowed by "
|
|
37
|
+
f"{risk.cheaper_sibling} (cost {risk.cheaper_sibling_invocation_cost}) — "
|
|
38
|
+
f"{risk.justification}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return "\n".join(lines) + "\n"
|
tdqs/cli/main.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from tdqs.cli.run_cli import run_cli
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
"""The one place the process boundary is crossed: argv, environment, the two streams and the
|
|
9
|
+
exit code. Everything behind `run_cli` is testable in-process."""
|
|
10
|
+
sys.exit(run_cli(argv=sys.argv[1:], env=os.environ, stderr=sys.stderr, stdout=sys.stdout))
|