@softspark/ai-toolkit 4.16.1 → 4.17.0
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.
- package/CHANGELOG.md +32 -0
- package/README.md +9 -15
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/hooks/session-end.sh +1 -13
- package/app/hooks.json +0 -10
- package/benchmarks/ecosystem-doctor-snapshot.json +14 -15
- package/bin/ai-toolkit.js +0 -2
- package/kb/history/completed/output-filter-retirement-20260726.md +128 -0
- package/kb/reference/architecture-overview.md +2 -3
- package/kb/reference/cli-reference.md +3 -13
- package/kb/reference/enterprise-config-guide.md +1 -21
- package/kb/reference/hooks-catalog.md +3 -60
- package/kb/reference/supported-tools-registry.md +0 -4
- package/llms-full.txt +143 -395
- package/llms.txt +1 -1
- package/manifest.json +147 -36
- package/package.json +1 -2
- package/scripts/claude_app.py +2 -21
- package/scripts/config_cli.py +4 -0
- package/scripts/config_merger.py +0 -17
- package/scripts/config_validator.py +11 -138
- package/scripts/doctor.py +3 -20
- package/scripts/install.py +2 -1
- package/scripts/install_steps/ai_tools.py +28 -99
- package/scripts/install_steps/hooks.py +26 -24
- package/scripts/merge-hooks.py +33 -2
- package/scripts/output_filter_retirement.py +395 -0
- package/scripts/schemas/ai-toolkit-config.schema.json +0 -60
- package/scripts/uninstall.py +13 -27
- package/app/hooks/filter-tool-output.sh +0 -76
- package/app/output-filter-policy.json +0 -15
- package/benchmarks/output-filter/README.md +0 -11
- package/benchmarks/output-filter/scenarios.json +0 -25
- package/kb/reference/tool-output-filter.md +0 -288
- package/scripts/benchmark_output_filter.py +0 -343
- package/scripts/output_filter_cli.py +0 -347
- package/scripts/output_filter_hook.py +0 -23
- package/scripts/tool_output_filter/__init__.py +0 -33
- package/scripts/tool_output_filter/contracts.py +0 -173
- package/scripts/tool_output_filter/engine.py +0 -260
- package/scripts/tool_output_filter/hook_runtime.py +0 -369
- package/scripts/tool_output_filter/input.py +0 -56
- package/scripts/tool_output_filter/invariants.py +0 -40
- package/scripts/tool_output_filter/policy.py +0 -153
- package/scripts/tool_output_filter/profiles/__init__.py +0 -68
- package/scripts/tool_output_filter/profiles/repeat_lines.py +0 -71
- package/scripts/tool_output_filter/profiles/tap_success.py +0 -154
- package/scripts/tool_output_filter/recovery.py +0 -846
- package/scripts/tool_output_filter/telemetry.py +0 -13
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"""Stateful safety invariants shared by runtime adapters."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
DEFAULT_FAILURE_LIMIT = 3
|
|
6
|
-
|
|
7
|
-
# "never-worse" (savings margin missed after the recovery marker) is a benign
|
|
8
|
-
# outcome, not a safety failure: nothing was emitted. Only genuine anomalies
|
|
9
|
-
# may open the breaker.
|
|
10
|
-
_CIRCUIT_FAILURES = frozenset(
|
|
11
|
-
{
|
|
12
|
-
"profile-failed",
|
|
13
|
-
"recovery-failed",
|
|
14
|
-
"recovery-verification-failed",
|
|
15
|
-
}
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SessionCircuitBreaker:
|
|
20
|
-
"""Open a session-local bypass after consecutive safety failures."""
|
|
21
|
-
|
|
22
|
-
__slots__ = ("failure_limit", "consecutive_failures")
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
failure_limit: int = DEFAULT_FAILURE_LIMIT,
|
|
27
|
-
consecutive_failures: int = 0,
|
|
28
|
-
) -> None:
|
|
29
|
-
self.failure_limit = failure_limit
|
|
30
|
-
self.consecutive_failures = consecutive_failures
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def is_open(self) -> bool:
|
|
34
|
-
return self.consecutive_failures >= self.failure_limit
|
|
35
|
-
|
|
36
|
-
def record(self, fallback_reason: str | None) -> None:
|
|
37
|
-
if fallback_reason in _CIRCUIT_FAILURES:
|
|
38
|
-
self.consecutive_failures += 1
|
|
39
|
-
else:
|
|
40
|
-
self.consecutive_failures = 0
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""Strict materialized policy contract."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
from .contracts import (
|
|
9
|
-
DEFAULT_MAX_INPUT_BYTES,
|
|
10
|
-
DEFAULT_MIN_SAVINGS_BYTES,
|
|
11
|
-
DEFAULT_MIN_SAVINGS_RATIO,
|
|
12
|
-
FilterMode,
|
|
13
|
-
)
|
|
14
|
-
from .recovery import DEFAULT_MAX_SESSION_BYTES, DEFAULT_TTL_MINUTES
|
|
15
|
-
|
|
16
|
-
_KNOWN_PROFILES = frozenset({"repeat-lines", "tap-success"})
|
|
17
|
-
_TOP_LEVEL_KEYS = frozenset(
|
|
18
|
-
{
|
|
19
|
-
"mode",
|
|
20
|
-
"profiles",
|
|
21
|
-
"maxInputBytes",
|
|
22
|
-
"minSavingsBytes",
|
|
23
|
-
"minSavingsRatio",
|
|
24
|
-
"recovery",
|
|
25
|
-
}
|
|
26
|
-
)
|
|
27
|
-
_RECOVERY_KEYS = frozenset({"mode", "ttlMinutes", "maxSessionBytes"})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class OutputFilterPolicy:
|
|
31
|
-
__slots__ = (
|
|
32
|
-
"max_input_bytes",
|
|
33
|
-
"max_session_bytes",
|
|
34
|
-
"min_savings_bytes",
|
|
35
|
-
"min_savings_ratio",
|
|
36
|
-
"mode",
|
|
37
|
-
"profiles",
|
|
38
|
-
"ttl_minutes",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self,
|
|
43
|
-
mode: FilterMode,
|
|
44
|
-
profiles: tuple[str, ...],
|
|
45
|
-
max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
|
|
46
|
-
min_savings_bytes: int = DEFAULT_MIN_SAVINGS_BYTES,
|
|
47
|
-
min_savings_ratio: float = DEFAULT_MIN_SAVINGS_RATIO,
|
|
48
|
-
ttl_minutes: int = DEFAULT_TTL_MINUTES,
|
|
49
|
-
max_session_bytes: int = DEFAULT_MAX_SESSION_BYTES,
|
|
50
|
-
) -> None:
|
|
51
|
-
self.mode = mode
|
|
52
|
-
self.profiles = profiles
|
|
53
|
-
self.max_input_bytes = max_input_bytes
|
|
54
|
-
self.min_savings_bytes = min_savings_bytes
|
|
55
|
-
self.min_savings_ratio = min_savings_ratio
|
|
56
|
-
self.ttl_minutes = ttl_minutes
|
|
57
|
-
self.max_session_bytes = max_session_bytes
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _integer(data: dict[str, object], key: str, default: int) -> int:
|
|
61
|
-
value = data.get(key, default)
|
|
62
|
-
if isinstance(value, bool) or not isinstance(value, int):
|
|
63
|
-
raise ValueError(f"{key} must be an integer")
|
|
64
|
-
return value
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _number(data: dict[str, object], key: str, default: float) -> float:
|
|
68
|
-
value = data.get(key, default)
|
|
69
|
-
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
70
|
-
raise ValueError(f"{key} must be a number")
|
|
71
|
-
return float(value)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _validate_ranges(
|
|
75
|
-
max_input_bytes: int,
|
|
76
|
-
min_savings_bytes: int,
|
|
77
|
-
min_savings_ratio: float,
|
|
78
|
-
ttl_minutes: int,
|
|
79
|
-
max_session_bytes: int,
|
|
80
|
-
) -> None:
|
|
81
|
-
if not 1 <= max_input_bytes <= DEFAULT_MAX_INPUT_BYTES:
|
|
82
|
-
raise ValueError("maxInputBytes is outside the safe range")
|
|
83
|
-
if not 0 <= min_savings_bytes <= max_input_bytes:
|
|
84
|
-
raise ValueError("minSavingsBytes is outside the safe range")
|
|
85
|
-
if not 0.0 <= min_savings_ratio <= 1.0:
|
|
86
|
-
raise ValueError("minSavingsRatio is outside the safe range")
|
|
87
|
-
if ttl_minutes <= 0 or max_session_bytes <= 0:
|
|
88
|
-
raise ValueError("recovery limits must be positive")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def load_policy(path: str | os.PathLike[str]) -> OutputFilterPolicy:
|
|
92
|
-
"""Load an unwrapped, materialized policy object."""
|
|
93
|
-
|
|
94
|
-
with open(path, encoding="utf-8") as policy_file:
|
|
95
|
-
data = json.load(policy_file)
|
|
96
|
-
if not isinstance(data, dict):
|
|
97
|
-
raise ValueError("output-filter policy must be an object")
|
|
98
|
-
if set(data) - _TOP_LEVEL_KEYS:
|
|
99
|
-
raise ValueError("output-filter policy contains unknown keys")
|
|
100
|
-
mode = FilterMode(data["mode"])
|
|
101
|
-
raw_profiles = data.get("profiles", [])
|
|
102
|
-
if not isinstance(raw_profiles, list) or not all(
|
|
103
|
-
isinstance(profile, str) for profile in raw_profiles
|
|
104
|
-
):
|
|
105
|
-
raise ValueError("profiles must be a string array")
|
|
106
|
-
if len(set(raw_profiles)) != len(raw_profiles):
|
|
107
|
-
raise ValueError("profiles must not contain duplicates")
|
|
108
|
-
if any(profile not in _KNOWN_PROFILES for profile in raw_profiles):
|
|
109
|
-
raise ValueError("profiles contains an unknown profile")
|
|
110
|
-
recovery = data.get("recovery", {})
|
|
111
|
-
if not isinstance(recovery, dict):
|
|
112
|
-
raise ValueError("recovery must be an object")
|
|
113
|
-
if set(recovery) - _RECOVERY_KEYS:
|
|
114
|
-
raise ValueError("recovery contains unknown keys")
|
|
115
|
-
if recovery.get("mode", "ephemeral") != "ephemeral":
|
|
116
|
-
raise ValueError("only ephemeral recovery is supported")
|
|
117
|
-
max_input_bytes = _integer(
|
|
118
|
-
data,
|
|
119
|
-
"maxInputBytes",
|
|
120
|
-
DEFAULT_MAX_INPUT_BYTES,
|
|
121
|
-
)
|
|
122
|
-
min_savings_bytes = _integer(
|
|
123
|
-
data,
|
|
124
|
-
"minSavingsBytes",
|
|
125
|
-
DEFAULT_MIN_SAVINGS_BYTES,
|
|
126
|
-
)
|
|
127
|
-
min_savings_ratio = _number(
|
|
128
|
-
data,
|
|
129
|
-
"minSavingsRatio",
|
|
130
|
-
DEFAULT_MIN_SAVINGS_RATIO,
|
|
131
|
-
)
|
|
132
|
-
ttl_minutes = _integer(recovery, "ttlMinutes", DEFAULT_TTL_MINUTES)
|
|
133
|
-
max_session_bytes = _integer(
|
|
134
|
-
recovery,
|
|
135
|
-
"maxSessionBytes",
|
|
136
|
-
DEFAULT_MAX_SESSION_BYTES,
|
|
137
|
-
)
|
|
138
|
-
_validate_ranges(
|
|
139
|
-
max_input_bytes,
|
|
140
|
-
min_savings_bytes,
|
|
141
|
-
min_savings_ratio,
|
|
142
|
-
ttl_minutes,
|
|
143
|
-
max_session_bytes,
|
|
144
|
-
)
|
|
145
|
-
return OutputFilterPolicy(
|
|
146
|
-
mode=mode,
|
|
147
|
-
profiles=tuple(raw_profiles),
|
|
148
|
-
max_input_bytes=max_input_bytes,
|
|
149
|
-
min_savings_bytes=min_savings_bytes,
|
|
150
|
-
min_savings_ratio=min_savings_ratio,
|
|
151
|
-
ttl_minutes=ttl_minutes,
|
|
152
|
-
max_session_bytes=max_session_bytes,
|
|
153
|
-
)
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""Built-in deterministic output profiles."""
|
|
2
|
-
|
|
3
|
-
from .repeat_lines import PROFILE_ID as REPEAT_LINES_ID
|
|
4
|
-
from .repeat_lines import PROFILE_VERSION as REPEAT_LINES_VERSION
|
|
5
|
-
from .repeat_lines import transform as transform_repeat_lines
|
|
6
|
-
from .tap_success import PROFILE_ID as TAP_SUCCESS_ID
|
|
7
|
-
from .tap_success import PROFILE_VERSION as TAP_SUCCESS_VERSION
|
|
8
|
-
from .tap_success import transform as transform_tap_success
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ProfileTransform:
|
|
12
|
-
__slots__ = ("accepted", "output", "profile_id", "profile_version")
|
|
13
|
-
|
|
14
|
-
def __init__(
|
|
15
|
-
self,
|
|
16
|
-
profile_id: str,
|
|
17
|
-
profile_version: int,
|
|
18
|
-
output: str,
|
|
19
|
-
accepted: bool,
|
|
20
|
-
) -> None:
|
|
21
|
-
self.profile_id = profile_id
|
|
22
|
-
self.profile_version = profile_version
|
|
23
|
-
self.output = output
|
|
24
|
-
self.accepted = accepted
|
|
25
|
-
|
|
26
|
-
def __eq__(self, other: object) -> bool:
|
|
27
|
-
if not isinstance(other, ProfileTransform):
|
|
28
|
-
return NotImplemented
|
|
29
|
-
return (
|
|
30
|
-
self.profile_id == other.profile_id
|
|
31
|
-
and self.profile_version == other.profile_version
|
|
32
|
-
and self.output == other.output
|
|
33
|
-
and self.accepted == other.accepted
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def apply_profile(
|
|
38
|
-
profile_id: str,
|
|
39
|
-
output: str,
|
|
40
|
-
) -> ProfileTransform | None:
|
|
41
|
-
if profile_id == REPEAT_LINES_ID:
|
|
42
|
-
return ProfileTransform(
|
|
43
|
-
profile_id,
|
|
44
|
-
REPEAT_LINES_VERSION,
|
|
45
|
-
transform_repeat_lines(output),
|
|
46
|
-
True,
|
|
47
|
-
)
|
|
48
|
-
if profile_id == TAP_SUCCESS_ID:
|
|
49
|
-
transformed = transform_tap_success(output)
|
|
50
|
-
return ProfileTransform(
|
|
51
|
-
profile_id,
|
|
52
|
-
TAP_SUCCESS_VERSION,
|
|
53
|
-
transformed if transformed is not None else output,
|
|
54
|
-
transformed is not None,
|
|
55
|
-
)
|
|
56
|
-
return None
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
__all__ = [
|
|
60
|
-
"ProfileTransform",
|
|
61
|
-
"REPEAT_LINES_ID",
|
|
62
|
-
"REPEAT_LINES_VERSION",
|
|
63
|
-
"TAP_SUCCESS_ID",
|
|
64
|
-
"TAP_SUCCESS_VERSION",
|
|
65
|
-
"apply_profile",
|
|
66
|
-
"transform_repeat_lines",
|
|
67
|
-
"transform_tap_success",
|
|
68
|
-
]
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Compact adjacent identical output lines."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
PROFILE_ID = "repeat-lines"
|
|
6
|
-
PROFILE_VERSION = 1
|
|
7
|
-
|
|
8
|
-
_DIAGNOSTIC_PREFIXES = (
|
|
9
|
-
"error",
|
|
10
|
-
"warning",
|
|
11
|
-
"warn:",
|
|
12
|
-
"fatal",
|
|
13
|
-
"fail",
|
|
14
|
-
"not ok",
|
|
15
|
-
"npm err!",
|
|
16
|
-
"critical",
|
|
17
|
-
"segmentation fault",
|
|
18
|
-
"traceback",
|
|
19
|
-
"exception",
|
|
20
|
-
"panic",
|
|
21
|
-
"assert",
|
|
22
|
-
"e ",
|
|
23
|
-
"✕",
|
|
24
|
-
"✗",
|
|
25
|
-
"security",
|
|
26
|
-
"vulnerab",
|
|
27
|
-
"cve-",
|
|
28
|
-
"denied",
|
|
29
|
-
"unauthor",
|
|
30
|
-
"permission",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _is_diagnostic(line: str) -> bool:
|
|
35
|
-
if not line.strip():
|
|
36
|
-
return True
|
|
37
|
-
stripped = line.lstrip().lower()
|
|
38
|
-
return (
|
|
39
|
-
stripped.startswith(_DIAGNOSTIC_PREFIXES)
|
|
40
|
-
or stripped.startswith("#")
|
|
41
|
-
or stripped.startswith("[ai-toolkit-output-filter ")
|
|
42
|
-
or "\x1b" in line
|
|
43
|
-
or "\x00" in line
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def transform(output: str) -> str:
|
|
48
|
-
"""Collapse adjacent identical lines while preserving their first copy."""
|
|
49
|
-
|
|
50
|
-
lines = output.splitlines(keepends=True)
|
|
51
|
-
if not lines:
|
|
52
|
-
return output
|
|
53
|
-
|
|
54
|
-
transformed: list[str] = []
|
|
55
|
-
index = 0
|
|
56
|
-
while index < len(lines):
|
|
57
|
-
line = lines[index]
|
|
58
|
-
run_end = index + 1
|
|
59
|
-
while run_end < len(lines) and lines[run_end] == line:
|
|
60
|
-
run_end += 1
|
|
61
|
-
run_length = run_end - index
|
|
62
|
-
transformed.append(line)
|
|
63
|
-
if run_length > 1 and not _is_diagnostic(line):
|
|
64
|
-
transformed.append(
|
|
65
|
-
"[ai-toolkit-output-filter repeat-lines/v1: "
|
|
66
|
-
f"{run_length - 1} adjacent copies omitted]\n"
|
|
67
|
-
)
|
|
68
|
-
elif run_length > 1:
|
|
69
|
-
transformed.extend(lines[index + 1 : run_end])
|
|
70
|
-
index = run_end
|
|
71
|
-
return "".join(transformed)
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
"""Conservative compaction for valid, fully successful TAP output."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
PROFILE_ID = "tap-success"
|
|
6
|
-
PROFILE_VERSION = 1
|
|
7
|
-
|
|
8
|
-
_DIAGNOSTIC_COMMENT_PREFIXES = (
|
|
9
|
-
"# error",
|
|
10
|
-
"# stack",
|
|
11
|
-
"# message",
|
|
12
|
-
"# operator",
|
|
13
|
-
"# expected",
|
|
14
|
-
"# actual",
|
|
15
|
-
"# severity",
|
|
16
|
-
"# at ",
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _line_ending(lines: list[str]) -> str:
|
|
21
|
-
for line in lines:
|
|
22
|
-
if line.endswith("\r\n"):
|
|
23
|
-
return "\r\n"
|
|
24
|
-
if line.endswith("\n"):
|
|
25
|
-
return "\n"
|
|
26
|
-
return "\n"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _parse_plan(line: str) -> int | None:
|
|
30
|
-
content = line.strip()
|
|
31
|
-
if not content.startswith("1.."):
|
|
32
|
-
return None
|
|
33
|
-
count_text = content[3:].split("#", 1)[0].strip()
|
|
34
|
-
if not count_text.isdecimal():
|
|
35
|
-
return None
|
|
36
|
-
return int(count_text)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _parse_ok_number(line: str) -> tuple[int, bool] | None:
|
|
40
|
-
content = line.strip()
|
|
41
|
-
if not content.startswith("ok "):
|
|
42
|
-
return None
|
|
43
|
-
remainder = content[3:]
|
|
44
|
-
number_text = remainder.split(maxsplit=1)[0]
|
|
45
|
-
if not number_text.isdecimal():
|
|
46
|
-
return None
|
|
47
|
-
has_directive = False
|
|
48
|
-
if "#" in content:
|
|
49
|
-
directive = content.rsplit("#", 1)[1].strip().upper()
|
|
50
|
-
if not directive.startswith(("SKIP", "TODO")):
|
|
51
|
-
return None
|
|
52
|
-
has_directive = True
|
|
53
|
-
return int(number_text), has_directive
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _has_nonzero_failure_summary(content: str) -> bool:
|
|
57
|
-
lowered = content.lower()
|
|
58
|
-
for prefix in ("# fail", "# cancel"):
|
|
59
|
-
if not lowered.startswith(prefix):
|
|
60
|
-
continue
|
|
61
|
-
remainder = lowered[len(prefix):]
|
|
62
|
-
# Accept spelling variants ("# failed", "# failures:") but treat any
|
|
63
|
-
# non-zero or unparseable count as a failure signal.
|
|
64
|
-
value = remainder.lstrip("abcdefghijklmnopqrstuvwxyz").strip(" :\t")
|
|
65
|
-
return not value.isdecimal() or int(value) != 0
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _parse_success_summary(content: str) -> tuple[str, int] | None:
|
|
70
|
-
lowered = content.lower()
|
|
71
|
-
for name in ("tests", "pass"):
|
|
72
|
-
prefix = f"# {name} "
|
|
73
|
-
if not lowered.startswith(prefix):
|
|
74
|
-
continue
|
|
75
|
-
value = lowered[len(prefix):].strip()
|
|
76
|
-
return name, int(value) if value.isdecimal() else -1
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def transform(output: str) -> str | None:
|
|
81
|
-
"""Return compact TAP, or ``None`` when the stream is not strictly safe."""
|
|
82
|
-
|
|
83
|
-
lines = output.splitlines(keepends=True)
|
|
84
|
-
if not lines or any(
|
|
85
|
-
"[ai-toolkit-output-filter " in line for line in lines
|
|
86
|
-
):
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
plan_count: int | None = None
|
|
90
|
-
result_numbers: list[int] = []
|
|
91
|
-
summary_counts: dict[str, int] = {}
|
|
92
|
-
omitted_count = 0
|
|
93
|
-
transformed: list[str] = []
|
|
94
|
-
newline = _line_ending(lines)
|
|
95
|
-
|
|
96
|
-
def flush_omitted() -> None:
|
|
97
|
-
nonlocal omitted_count
|
|
98
|
-
if omitted_count:
|
|
99
|
-
transformed.append(
|
|
100
|
-
"[ai-toolkit-output-filter tap-success/v1: "
|
|
101
|
-
f"{omitted_count} successful result lines omitted]"
|
|
102
|
-
f"{newline}"
|
|
103
|
-
)
|
|
104
|
-
omitted_count = 0
|
|
105
|
-
|
|
106
|
-
for line in lines:
|
|
107
|
-
content = line.strip()
|
|
108
|
-
if content == "TAP version 13":
|
|
109
|
-
flush_omitted()
|
|
110
|
-
transformed.append(line)
|
|
111
|
-
continue
|
|
112
|
-
parsed_plan = _parse_plan(line)
|
|
113
|
-
if parsed_plan is not None:
|
|
114
|
-
if plan_count is not None:
|
|
115
|
-
return None
|
|
116
|
-
plan_count = parsed_plan
|
|
117
|
-
flush_omitted()
|
|
118
|
-
transformed.append(line)
|
|
119
|
-
continue
|
|
120
|
-
parsed_ok = _parse_ok_number(line)
|
|
121
|
-
if parsed_ok is not None:
|
|
122
|
-
number, has_directive = parsed_ok
|
|
123
|
-
result_numbers.append(number)
|
|
124
|
-
if has_directive:
|
|
125
|
-
flush_omitted()
|
|
126
|
-
transformed.append(line)
|
|
127
|
-
else:
|
|
128
|
-
omitted_count += 1
|
|
129
|
-
continue
|
|
130
|
-
if content.lower().startswith(_DIAGNOSTIC_COMMENT_PREFIXES):
|
|
131
|
-
return None
|
|
132
|
-
if _has_nonzero_failure_summary(content):
|
|
133
|
-
return None
|
|
134
|
-
parsed_summary = _parse_success_summary(content)
|
|
135
|
-
if parsed_summary is not None:
|
|
136
|
-
name, value = parsed_summary
|
|
137
|
-
if value < 0 or name in summary_counts:
|
|
138
|
-
return None
|
|
139
|
-
summary_counts[name] = value
|
|
140
|
-
if not content or content.startswith("#"):
|
|
141
|
-
flush_omitted()
|
|
142
|
-
transformed.append(line)
|
|
143
|
-
continue
|
|
144
|
-
return None
|
|
145
|
-
|
|
146
|
-
flush_omitted()
|
|
147
|
-
if (
|
|
148
|
-
plan_count is None
|
|
149
|
-
or plan_count == 0
|
|
150
|
-
or result_numbers != list(range(1, plan_count + 1))
|
|
151
|
-
or any(count != plan_count for count in summary_counts.values())
|
|
152
|
-
):
|
|
153
|
-
return None
|
|
154
|
-
return "".join(transformed)
|