@mcptoolshop/accessibility-suite 0.1.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/.github/workflows/ci.yml +63 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/docs/prov-spec/.github/workflows/ci.yml +68 -0
- package/docs/prov-spec/CHANGELOG.md +69 -0
- package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
- package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
- package/docs/prov-spec/CONTRIBUTING.md +145 -0
- package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
- package/docs/prov-spec/LICENSE +21 -0
- package/docs/prov-spec/PRESS_RELEASE.md +74 -0
- package/docs/prov-spec/README.md +182 -0
- package/docs/prov-spec/SETUP.md +135 -0
- package/docs/prov-spec/WHY.md +86 -0
- package/docs/prov-spec/examples/artifact.example.json +14 -0
- package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
- package/docs/prov-spec/examples/evidence.example.json +6 -0
- package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
- package/docs/prov-spec/examples/mcp.request.example.json +28 -0
- package/docs/prov-spec/examples/prov.record.example.json +35 -0
- package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
- package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
- package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
- package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
- package/docs/prov-spec/spec/methods.json +213 -0
- package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
- package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
- package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
- package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
- package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
- package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
- package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
- package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
- package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
- package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
- package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
- package/docs/prov-spec/tools/python/prov_validator.py +428 -0
- package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
- package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
- package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
- package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
- package/examples/a11y-demo-site/LICENSE +21 -0
- package/examples/a11y-demo-site/README.md +155 -0
- package/examples/a11y-demo-site/html/contact.html +15 -0
- package/examples/a11y-demo-site/html/index.html +20 -0
- package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
- package/package.json +26 -0
- package/src/a11y-assist/.github/workflows/publish.yml +52 -0
- package/src/a11y-assist/.github/workflows/test.yml +30 -0
- package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
- package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-assist/CONTRIBUTING.md +98 -0
- package/src/a11y-assist/ENGINE.md +363 -0
- package/src/a11y-assist/LICENSE +21 -0
- package/src/a11y-assist/PRESS_RELEASE.md +71 -0
- package/src/a11y-assist/QUICKSTART.md +101 -0
- package/src/a11y-assist/README.md +192 -0
- package/src/a11y-assist/RELEASE_NOTES.md +319 -0
- package/src/a11y-assist/a11y_assist/__init__.py +3 -0
- package/src/a11y-assist/a11y_assist/cli.py +599 -0
- package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
- package/src/a11y-assist/a11y_assist/guard.py +444 -0
- package/src/a11y-assist/a11y_assist/ingest.py +407 -0
- package/src/a11y-assist/a11y_assist/methods.py +137 -0
- package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
- package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
- package/src/a11y-assist/a11y_assist/render.py +95 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
- package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
- package/src/a11y-assist/a11y_assist/storage.py +31 -0
- package/src/a11y-assist/pyproject.toml +60 -0
- package/src/a11y-assist/tests/__init__.py +1 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
- package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
- package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
- package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
- package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
- package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
- package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
- package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
- package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
- package/src/a11y-assist/tests/test_dyslexia.py +337 -0
- package/src/a11y-assist/tests/test_explain.py +74 -0
- package/src/a11y-assist/tests/test_golden.py +127 -0
- package/src/a11y-assist/tests/test_guard.py +819 -0
- package/src/a11y-assist/tests/test_guard_integration.py +457 -0
- package/src/a11y-assist/tests/test_ingest.py +311 -0
- package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
- package/src/a11y-assist/tests/test_plain_language.py +348 -0
- package/src/a11y-assist/tests/test_render.py +117 -0
- package/src/a11y-assist/tests/test_screen_reader.py +703 -0
- package/src/a11y-assist/tests/test_storage_last.py +61 -0
- package/src/a11y-assist/tests/test_triage.py +86 -0
- package/src/a11y-ci/.github/workflows/ci.yml +43 -0
- package/src/a11y-ci/.github/workflows/test.yml +30 -0
- package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
- package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-ci/CONTRIBUTING.md +142 -0
- package/src/a11y-ci/LICENSE +21 -0
- package/src/a11y-ci/README.md +105 -0
- package/src/a11y-ci/a11y_ci/__init__.py +3 -0
- package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
- package/src/a11y-ci/a11y_ci/cli.py +145 -0
- package/src/a11y-ci/a11y_ci/gate.py +131 -0
- package/src/a11y-ci/a11y_ci/render.py +48 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
- package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
- package/src/a11y-ci/npm/package.json +35 -0
- package/src/a11y-ci/pyproject.toml +64 -0
- package/src/a11y-ci/tests/__init__.py +1 -0
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
- package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
- package/src/a11y-ci/tests/test_gate.py +134 -0
- package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
- package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
- package/src/a11y-evidence-engine/LICENSE +21 -0
- package/src/a11y-evidence-engine/README.md +71 -0
- package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
- package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
- package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
- package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
- package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
- package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
- package/src/a11y-evidence-engine/package-lock.json +109 -0
- package/src/a11y-evidence-engine/package.json +45 -0
- package/src/a11y-evidence-engine/src/cli.js +74 -0
- package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
- package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
- package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
- package/src/a11y-evidence-engine/src/fswalk.js +56 -0
- package/src/a11y-evidence-engine/src/html_parse.js +117 -0
- package/src/a11y-evidence-engine/src/ids.js +53 -0
- package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
- package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
- package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
- package/src/a11y-evidence-engine/src/rules/index.js +37 -0
- package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
- package/src/a11y-evidence-engine/src/scan.js +128 -0
- package/src/a11y-evidence-engine/test/scan.test.js +149 -0
- package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
- package/src/a11y-lint/.github/workflows/ci.yml +46 -0
- package/src/a11y-lint/.github/workflows/test.yml +34 -0
- package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-lint/CONTRIBUTING.md +70 -0
- package/src/a11y-lint/GOVERNANCE.md +57 -0
- package/src/a11y-lint/LICENSE +21 -0
- package/src/a11y-lint/PRESS_RELEASE.md +50 -0
- package/src/a11y-lint/README.md +276 -0
- package/src/a11y-lint/RELEASE_NOTES.md +57 -0
- package/src/a11y-lint/RELEASING.md +57 -0
- package/src/a11y-lint/a11y_lint/__init__.py +64 -0
- package/src/a11y-lint/a11y_lint/cli.py +319 -0
- package/src/a11y-lint/a11y_lint/errors.py +252 -0
- package/src/a11y-lint/a11y_lint/render.py +293 -0
- package/src/a11y-lint/a11y_lint/report_md.py +289 -0
- package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
- package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
- package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
- package/src/a11y-lint/a11y_lint/validate.py +225 -0
- package/src/a11y-lint/pyproject.toml +75 -0
- package/src/a11y-lint/tests/__init__.py +1 -0
- package/src/a11y-lint/tests/test_cli.py +200 -0
- package/src/a11y-lint/tests/test_errors.py +188 -0
- package/src/a11y-lint/tests/test_render.py +202 -0
- package/src/a11y-lint/tests/test_report_md.py +188 -0
- package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
- package/src/a11y-lint/tests/test_scorecard.py +195 -0
- package/src/a11y-lint/tests/test_validate.py +257 -0
- package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
- package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
- package/src/a11y-mcp-tools/LICENSE +21 -0
- package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
- package/src/a11y-mcp-tools/README.md +168 -0
- package/src/a11y-mcp-tools/bin/cli.js +452 -0
- package/src/a11y-mcp-tools/bin/server.js +244 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/src/a11y-mcp-tools/package-lock.json +189 -0
- package/src/a11y-mcp-tools/package.json +49 -0
- package/src/a11y-mcp-tools/src/envelope.js +197 -0
- package/src/a11y-mcp-tools/src/index.js +9 -0
- package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
- package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
- package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
- package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
- package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
- package/src/a11y-mcp-tools/src/tools/index.js +10 -0
- package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
- package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
- package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
- package/src/a11y-mcp-tools/test/schema.test.js +327 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Load and validate cli.error.v0.1, produce deterministic assist.
|
|
2
|
+
|
|
3
|
+
High-confidence path: validated JSON with ID, What, Why, Fix.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Tuple
|
|
12
|
+
|
|
13
|
+
from jsonschema import Draft202012Validator
|
|
14
|
+
|
|
15
|
+
from .methods import METHOD_NORMALIZE_CLI_ERROR, evidence_for_plan
|
|
16
|
+
from .render import AssistResult, Evidence
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_schema(name: str) -> Dict[str, Any]:
|
|
20
|
+
"""Load a JSON schema from the schemas package."""
|
|
21
|
+
with resources.files("a11y_assist.schemas").joinpath(name).open("rb") as f:
|
|
22
|
+
return json.load(f)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_CLI_ERROR_SCHEMA = _load_schema("cli.error.schema.v0.1.json")
|
|
26
|
+
_CLI_ERROR_VALIDATOR = Draft202012Validator(_CLI_ERROR_SCHEMA)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CliErrorValidationError(Exception):
|
|
30
|
+
"""Raised when cli.error.v0.1 validation fails."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, errors: List[str]):
|
|
33
|
+
super().__init__("cli.error.v0.1 validation failed")
|
|
34
|
+
self.errors = errors
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_cli_error(path: str) -> Dict[str, Any]:
|
|
38
|
+
"""Load and validate a cli.error.v0.1 JSON file."""
|
|
39
|
+
obj = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
40
|
+
errs: List[str] = []
|
|
41
|
+
for e in sorted(_CLI_ERROR_VALIDATOR.iter_errors(obj), key=lambda x: x.path):
|
|
42
|
+
loc = ".".join([str(p) for p in e.path]) or "(root)"
|
|
43
|
+
errs.append(f"{loc}: {e.message}")
|
|
44
|
+
if errs:
|
|
45
|
+
raise CliErrorValidationError(errs)
|
|
46
|
+
return obj
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalize_to_list(value: Any) -> List[str]:
|
|
50
|
+
"""Normalize a value to a list of strings (handles both string and array formats)."""
|
|
51
|
+
if value is None:
|
|
52
|
+
return []
|
|
53
|
+
if isinstance(value, str):
|
|
54
|
+
return [value] if value.strip() else []
|
|
55
|
+
if isinstance(value, list):
|
|
56
|
+
return [str(v) for v in value if v]
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def assist_from_cli_error(obj: Dict[str, Any]) -> AssistResult:
|
|
61
|
+
"""Generate an AssistResult from a validated cli.error.v0.1 object.
|
|
62
|
+
|
|
63
|
+
Deterministic: no guessing. Builds plan from Fix lines.
|
|
64
|
+
Handles both string and array formats for what/why/fix.
|
|
65
|
+
"""
|
|
66
|
+
# Support both 'id' and 'code' fields for ID
|
|
67
|
+
err_id = obj.get("id") or obj.get("code")
|
|
68
|
+
title = obj.get("title") or obj.get("what", "Issue")
|
|
69
|
+
if isinstance(title, list):
|
|
70
|
+
title = title[0] if title else "Issue"
|
|
71
|
+
|
|
72
|
+
# Normalize to lists
|
|
73
|
+
what = _normalize_to_list(obj.get("what"))
|
|
74
|
+
why = _normalize_to_list(obj.get("why"))
|
|
75
|
+
fix = _normalize_to_list(obj.get("fix"))
|
|
76
|
+
|
|
77
|
+
# Build plan from Fix lines
|
|
78
|
+
plan: List[str] = []
|
|
79
|
+
for line in fix:
|
|
80
|
+
if isinstance(line, str) and line.strip():
|
|
81
|
+
plan.append(line.strip())
|
|
82
|
+
|
|
83
|
+
if not plan:
|
|
84
|
+
plan = ["Follow the Fix steps provided by the tool output."]
|
|
85
|
+
|
|
86
|
+
safest_next = "Follow the Fix steps in order, starting with the least risky check."
|
|
87
|
+
if why and isinstance(why[0], str) and why[0].strip():
|
|
88
|
+
safest_next = (
|
|
89
|
+
"Start by confirming the cause described under 'Why', "
|
|
90
|
+
"then apply the first Fix step."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# SAFE commands: only include clearly non-destructive suggestions from fix text.
|
|
94
|
+
# v0.1 is conservative: we only surface commands already present (not invented).
|
|
95
|
+
next_cmds: List[str] = []
|
|
96
|
+
for line in fix:
|
|
97
|
+
if isinstance(line, str):
|
|
98
|
+
# Accept explicit dry-run or command prefixes
|
|
99
|
+
if "--dry-run" in line or line.strip().startswith(("$ ", "> ", "run ")):
|
|
100
|
+
next_cmds.append(line.replace("$", "").replace(">", "").strip())
|
|
101
|
+
# Accept "Re-run: <cmd>" style
|
|
102
|
+
if line.lower().startswith("re-run:"):
|
|
103
|
+
next_cmds.append(line.split(":", 1)[1].strip())
|
|
104
|
+
|
|
105
|
+
# Filter to SAFE-only heuristically
|
|
106
|
+
safe_filtered = [
|
|
107
|
+
c for c in next_cmds if "--dry-run" in c or "validate" in c or "check" in c
|
|
108
|
+
]
|
|
109
|
+
safe_filtered = list(dict.fromkeys(safe_filtered)) # dedupe preserving order
|
|
110
|
+
|
|
111
|
+
notes = [
|
|
112
|
+
f"Original title: {title}",
|
|
113
|
+
"This assist block is additive; it does not replace the tool's output.",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
# Build evidence anchors for traceability
|
|
117
|
+
evidence: List[Evidence] = []
|
|
118
|
+
|
|
119
|
+
# Evidence for safest_next_step
|
|
120
|
+
if why:
|
|
121
|
+
evidence.append(Evidence(field="safest_next_step", source="cli.error.why[0]"))
|
|
122
|
+
else:
|
|
123
|
+
evidence.append(Evidence(field="safest_next_step", source="cli.error.fix[0]"))
|
|
124
|
+
|
|
125
|
+
# Evidence for plan steps (map to fix lines)
|
|
126
|
+
evidence.extend(evidence_for_plan(plan, source_prefix="cli.error.fix"))
|
|
127
|
+
|
|
128
|
+
# Evidence for safe commands (track which fix line they came from)
|
|
129
|
+
for i, cmd in enumerate(safe_filtered[:3]):
|
|
130
|
+
# Find the original fix line index
|
|
131
|
+
for j, fix_line in enumerate(fix):
|
|
132
|
+
if cmd in fix_line or (
|
|
133
|
+
fix_line.lower().startswith("re-run:") and cmd == fix_line.split(":", 1)[1].strip()
|
|
134
|
+
):
|
|
135
|
+
evidence.append(
|
|
136
|
+
Evidence(field=f"next_safe_commands[{i}]", source=f"cli.error.fix[{j}]")
|
|
137
|
+
)
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
return AssistResult(
|
|
141
|
+
anchored_id=err_id if isinstance(err_id, str) else None,
|
|
142
|
+
confidence="High",
|
|
143
|
+
safest_next_step=safest_next,
|
|
144
|
+
plan=plan,
|
|
145
|
+
next_safe_commands=safe_filtered[:3],
|
|
146
|
+
notes=notes,
|
|
147
|
+
methods_applied=(METHOD_NORMALIZE_CLI_ERROR,),
|
|
148
|
+
evidence=tuple(evidence),
|
|
149
|
+
)
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Profile Guard: centralized invariant checker for profile transforms.
|
|
2
|
+
|
|
3
|
+
Runs after every profile transform to prevent unsafe drift.
|
|
4
|
+
Guard failures are engine bugs, not user errors.
|
|
5
|
+
|
|
6
|
+
Invariants enforced:
|
|
7
|
+
1. Anchored ID cannot be invented or changed
|
|
8
|
+
2. Confidence cannot increase
|
|
9
|
+
3. SAFE-only commands: no new commands, no risky
|
|
10
|
+
4. Step count caps enforced
|
|
11
|
+
5. Profile must not add new factual content
|
|
12
|
+
6. Profile-specific constraints (parentheticals, visual refs)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Dict, List, Literal, Optional, Set
|
|
20
|
+
|
|
21
|
+
from .render import AssistResult, Confidence
|
|
22
|
+
|
|
23
|
+
Severity = Literal["ERROR", "WARN"]
|
|
24
|
+
|
|
25
|
+
# Stopwords to ignore in content overlap checking
|
|
26
|
+
STOPWORDS = frozenset([
|
|
27
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
28
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
29
|
+
"should", "may", "might", "must", "shall", "can", "to", "of", "in",
|
|
30
|
+
"for", "on", "with", "at", "by", "from", "as", "into", "through",
|
|
31
|
+
"during", "before", "after", "above", "below", "between", "under",
|
|
32
|
+
"again", "further", "then", "once", "here", "there", "when", "where",
|
|
33
|
+
"why", "how", "all", "each", "few", "more", "most", "other", "some",
|
|
34
|
+
"such", "no", "nor", "not", "only", "own", "same", "so", "than",
|
|
35
|
+
"too", "very", "just", "also", "now", "and", "but", "or", "if", "it",
|
|
36
|
+
"its", "this", "that", "these", "those", "what", "which", "who",
|
|
37
|
+
"whom", "your", "you", "we", "they", "them", "their", "our", "my",
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
# Allowed glue vocabulary for plan steps (common action words)
|
|
41
|
+
GLUE_VOCABULARY = frozenset([
|
|
42
|
+
"step", "first", "next", "last", "run", "rerun", "re-run", "confirm",
|
|
43
|
+
"check", "verify", "try", "retry", "follow", "start", "continue",
|
|
44
|
+
"do", "ensure", "make", "see", "look", "update", "fix", "apply",
|
|
45
|
+
"tool", "tools", "command", "commands", "output", "input", "file",
|
|
46
|
+
"files", "error", "errors", "warning", "warnings", "dry", "dryrun",
|
|
47
|
+
"dry-run", "validate", "validation", "config", "configuration",
|
|
48
|
+
"line", "cli", "json", "order", "instructions", "steps",
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
# Visual navigation patterns
|
|
52
|
+
VISUAL_NAV_PATTERNS = re.compile(
|
|
53
|
+
r"\b(see\s+)?(above|below|left|right|arrow)\b", re.IGNORECASE
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Parenthetical pattern
|
|
57
|
+
PARENTHETICAL_PATTERN = re.compile(r"[\(\)\[\]]")
|
|
58
|
+
|
|
59
|
+
# Confidence ordering (lower index = lower confidence)
|
|
60
|
+
CONFIDENCE_ORDER = {"Low": 0, "Medium": 1, "High": 2}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class GuardIssue:
|
|
65
|
+
"""A single guard violation."""
|
|
66
|
+
|
|
67
|
+
severity: Severity
|
|
68
|
+
code: str # e.g. A11Y.ASSIST.GUARD.COMMANDS.INVENTED
|
|
69
|
+
message: str # human-readable
|
|
70
|
+
details: Dict[str, str] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GuardViolation(Exception):
|
|
74
|
+
"""Exception raised when profile transform violates invariants."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, issues: List[GuardIssue]):
|
|
77
|
+
super().__init__("Profile guard violation")
|
|
78
|
+
self.issues = issues
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
lines = ["Profile guard violation:"]
|
|
82
|
+
for issue in self.issues:
|
|
83
|
+
lines.append(f" [{issue.severity}] {issue.code}: {issue.message}")
|
|
84
|
+
for k, v in issue.details.items():
|
|
85
|
+
lines.append(f" {k}: {v}")
|
|
86
|
+
return "\n".join(lines)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class GuardContext:
|
|
91
|
+
"""Context for guard validation."""
|
|
92
|
+
|
|
93
|
+
profile: str # e.g. "screen-reader"
|
|
94
|
+
confidence: Confidence # High/Medium/Low
|
|
95
|
+
input_kind: str # cli_error_json|raw_text|scorecard_json|last_log
|
|
96
|
+
allowed_safe_commands: frozenset[str] # derived from base inputs verbatim
|
|
97
|
+
|
|
98
|
+
# Per-profile constraints
|
|
99
|
+
forbid_parentheticals: bool = False
|
|
100
|
+
forbid_visual_refs: bool = False
|
|
101
|
+
max_steps: Optional[int] = None # enforce if set
|
|
102
|
+
allow_commands_on_low: bool = False # default: no commands on Low confidence
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _tokenize_content(text: str) -> Set[str]:
|
|
106
|
+
"""Tokenize text into lowercase content words.
|
|
107
|
+
|
|
108
|
+
- Letters/numbers only
|
|
109
|
+
- Strip punctuation
|
|
110
|
+
- Drop stopwords
|
|
111
|
+
- Drop tokens < 3 chars
|
|
112
|
+
"""
|
|
113
|
+
# Extract alphanumeric tokens
|
|
114
|
+
tokens = re.findall(r"[a-zA-Z0-9]+", text.lower())
|
|
115
|
+
# Filter
|
|
116
|
+
return {
|
|
117
|
+
t for t in tokens
|
|
118
|
+
if len(t) >= 3 and t not in STOPWORDS
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _is_content_supported(line: str, base_tokens: Set[str]) -> bool:
|
|
123
|
+
"""Check if a line is supported by base text content.
|
|
124
|
+
|
|
125
|
+
A line is supported if:
|
|
126
|
+
- It shares at least one content word with base text, OR
|
|
127
|
+
- It's composed solely of glue vocabulary + base content words
|
|
128
|
+
"""
|
|
129
|
+
line_tokens = _tokenize_content(line)
|
|
130
|
+
|
|
131
|
+
if not line_tokens:
|
|
132
|
+
# Empty line or all stopwords/short words - allowed
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# Check for overlap with base
|
|
136
|
+
overlap = line_tokens & base_tokens
|
|
137
|
+
if overlap:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
# Check if all tokens are glue vocabulary
|
|
141
|
+
non_glue = line_tokens - GLUE_VOCABULARY
|
|
142
|
+
if not non_glue:
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
# Check if non-glue tokens are in base
|
|
146
|
+
unsupported = non_glue - base_tokens
|
|
147
|
+
return len(unsupported) == 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _check_id_invariant(
|
|
151
|
+
base: AssistResult, profiled: AssistResult, issues: List[GuardIssue]
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Check: Anchored ID cannot be invented or changed."""
|
|
154
|
+
if base.anchored_id is None:
|
|
155
|
+
if profiled.anchored_id is not None:
|
|
156
|
+
issues.append(GuardIssue(
|
|
157
|
+
severity="ERROR",
|
|
158
|
+
code="A11Y.ASSIST.GUARD.ID.INVENTED",
|
|
159
|
+
message="Profile invented an anchored ID that didn't exist in base",
|
|
160
|
+
details={
|
|
161
|
+
"base_id": "None",
|
|
162
|
+
"profiled_id": str(profiled.anchored_id),
|
|
163
|
+
},
|
|
164
|
+
))
|
|
165
|
+
else:
|
|
166
|
+
if profiled.anchored_id != base.anchored_id:
|
|
167
|
+
issues.append(GuardIssue(
|
|
168
|
+
severity="ERROR",
|
|
169
|
+
code="A11Y.ASSIST.GUARD.ID.CHANGED",
|
|
170
|
+
message="Profile changed the anchored ID",
|
|
171
|
+
details={
|
|
172
|
+
"base_id": str(base.anchored_id),
|
|
173
|
+
"profiled_id": str(profiled.anchored_id),
|
|
174
|
+
},
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _check_confidence_invariant(
|
|
179
|
+
base: AssistResult, profiled: AssistResult, issues: List[GuardIssue]
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Check: Confidence cannot increase."""
|
|
182
|
+
base_level = CONFIDENCE_ORDER.get(base.confidence, 0)
|
|
183
|
+
profiled_level = CONFIDENCE_ORDER.get(profiled.confidence, 0)
|
|
184
|
+
|
|
185
|
+
if profiled_level > base_level:
|
|
186
|
+
issues.append(GuardIssue(
|
|
187
|
+
severity="ERROR",
|
|
188
|
+
code="A11Y.ASSIST.GUARD.CONFIDENCE.INCREASED",
|
|
189
|
+
message="Profile increased confidence level (not allowed)",
|
|
190
|
+
details={
|
|
191
|
+
"base_confidence": base.confidence,
|
|
192
|
+
"profiled_confidence": profiled.confidence,
|
|
193
|
+
},
|
|
194
|
+
))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _check_commands_invariant(
|
|
198
|
+
base: AssistResult,
|
|
199
|
+
profiled: AssistResult,
|
|
200
|
+
ctx: GuardContext,
|
|
201
|
+
issues: List[GuardIssue],
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Check: SAFE-only commands - no new commands, no risky."""
|
|
204
|
+
# Check each profiled command
|
|
205
|
+
for cmd in profiled.next_safe_commands:
|
|
206
|
+
# Normalize for comparison (strip $ prefix)
|
|
207
|
+
normalized_cmd = cmd.lstrip("$ ").strip()
|
|
208
|
+
|
|
209
|
+
# Check if command is in allowed set
|
|
210
|
+
allowed = False
|
|
211
|
+
for allowed_cmd in ctx.allowed_safe_commands:
|
|
212
|
+
normalized_allowed = allowed_cmd.lstrip("$ ").strip()
|
|
213
|
+
if normalized_cmd == normalized_allowed:
|
|
214
|
+
allowed = True
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
if not allowed:
|
|
218
|
+
issues.append(GuardIssue(
|
|
219
|
+
severity="ERROR",
|
|
220
|
+
code="A11Y.ASSIST.GUARD.COMMANDS.INVENTED",
|
|
221
|
+
message="Profile included a command not in the allowed set",
|
|
222
|
+
details={
|
|
223
|
+
"command": cmd,
|
|
224
|
+
"allowed_commands": ", ".join(ctx.allowed_safe_commands) or "(none)",
|
|
225
|
+
},
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
# Check Low confidence rule
|
|
229
|
+
if ctx.confidence == "Low" and not ctx.allow_commands_on_low:
|
|
230
|
+
if profiled.next_safe_commands:
|
|
231
|
+
issues.append(GuardIssue(
|
|
232
|
+
severity="ERROR",
|
|
233
|
+
code="A11Y.ASSIST.GUARD.COMMANDS.DISALLOWED_LOW_CONF",
|
|
234
|
+
message="Profile included commands on Low confidence (not allowed)",
|
|
235
|
+
details={
|
|
236
|
+
"commands": ", ".join(profiled.next_safe_commands),
|
|
237
|
+
},
|
|
238
|
+
))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _check_step_count_invariant(
|
|
242
|
+
profiled: AssistResult, ctx: GuardContext, issues: List[GuardIssue]
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Check: Step count caps enforced."""
|
|
245
|
+
if ctx.max_steps is not None:
|
|
246
|
+
if len(profiled.plan) > ctx.max_steps:
|
|
247
|
+
issues.append(GuardIssue(
|
|
248
|
+
severity="ERROR",
|
|
249
|
+
code="A11Y.ASSIST.GUARD.PLAN.TOO_MANY_STEPS",
|
|
250
|
+
message=f"Profile exceeded max steps ({ctx.max_steps})",
|
|
251
|
+
details={
|
|
252
|
+
"max_steps": str(ctx.max_steps),
|
|
253
|
+
"actual_steps": str(len(profiled.plan)),
|
|
254
|
+
},
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _check_content_support_invariant(
|
|
259
|
+
base_text: str,
|
|
260
|
+
profiled: AssistResult,
|
|
261
|
+
issues: List[GuardIssue],
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Check: Profile must not add new factual content."""
|
|
264
|
+
base_tokens = _tokenize_content(base_text)
|
|
265
|
+
|
|
266
|
+
# Check safest_next_step
|
|
267
|
+
if not _is_content_supported(profiled.safest_next_step, base_tokens):
|
|
268
|
+
issues.append(GuardIssue(
|
|
269
|
+
severity="WARN",
|
|
270
|
+
code="A11Y.ASSIST.GUARD.CONTENT.UNSUPPORTED",
|
|
271
|
+
message="Safest next step contains content not found in base text",
|
|
272
|
+
details={
|
|
273
|
+
"text": profiled.safest_next_step[:80],
|
|
274
|
+
},
|
|
275
|
+
))
|
|
276
|
+
|
|
277
|
+
# Check plan steps
|
|
278
|
+
for i, step in enumerate(profiled.plan):
|
|
279
|
+
if not _is_content_supported(step, base_tokens):
|
|
280
|
+
issues.append(GuardIssue(
|
|
281
|
+
severity="WARN",
|
|
282
|
+
code="A11Y.ASSIST.GUARD.CONTENT.UNSUPPORTED",
|
|
283
|
+
message=f"Plan step {i + 1} contains content not found in base text",
|
|
284
|
+
details={
|
|
285
|
+
"step": step[:80],
|
|
286
|
+
},
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _check_parentheticals_constraint(
|
|
291
|
+
profiled: AssistResult, issues: List[GuardIssue]
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Check: No parentheticals allowed (profile-specific)."""
|
|
294
|
+
fields_to_check = [
|
|
295
|
+
("safest_next_step", profiled.safest_next_step),
|
|
296
|
+
]
|
|
297
|
+
fields_to_check.extend(
|
|
298
|
+
(f"plan[{i}]", step) for i, step in enumerate(profiled.plan)
|
|
299
|
+
)
|
|
300
|
+
fields_to_check.extend(
|
|
301
|
+
(f"notes[{i}]", note) for i, note in enumerate(profiled.notes)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
for field_name, text in fields_to_check:
|
|
305
|
+
if PARENTHETICAL_PATTERN.search(text):
|
|
306
|
+
issues.append(GuardIssue(
|
|
307
|
+
severity="ERROR",
|
|
308
|
+
code="A11Y.ASSIST.GUARD.TEXT.PARENTHETICALS_FORBIDDEN",
|
|
309
|
+
message=f"Parentheticals found in {field_name} (forbidden by profile)",
|
|
310
|
+
details={
|
|
311
|
+
"field": field_name,
|
|
312
|
+
"text": text[:80],
|
|
313
|
+
},
|
|
314
|
+
))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _check_visual_refs_constraint(
|
|
318
|
+
profiled: AssistResult, issues: List[GuardIssue]
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Check: No visual navigation references (profile-specific)."""
|
|
321
|
+
fields_to_check = [
|
|
322
|
+
("safest_next_step", profiled.safest_next_step),
|
|
323
|
+
]
|
|
324
|
+
fields_to_check.extend(
|
|
325
|
+
(f"plan[{i}]", step) for i, step in enumerate(profiled.plan)
|
|
326
|
+
)
|
|
327
|
+
fields_to_check.extend(
|
|
328
|
+
(f"notes[{i}]", note) for i, note in enumerate(profiled.notes)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
for field_name, text in fields_to_check:
|
|
332
|
+
if VISUAL_NAV_PATTERNS.search(text):
|
|
333
|
+
issues.append(GuardIssue(
|
|
334
|
+
severity="ERROR",
|
|
335
|
+
code="A11Y.ASSIST.GUARD.TEXT.VISUAL_REFS_FORBIDDEN",
|
|
336
|
+
message=f"Visual navigation reference found in {field_name} (forbidden by profile)",
|
|
337
|
+
details={
|
|
338
|
+
"field": field_name,
|
|
339
|
+
"text": text[:80],
|
|
340
|
+
},
|
|
341
|
+
))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def validate_profile_transform(
|
|
345
|
+
base_text: str,
|
|
346
|
+
base_result: AssistResult,
|
|
347
|
+
profiled_result: AssistResult,
|
|
348
|
+
ctx: GuardContext,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Validate that a profile transform respects all invariants.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
base_text: The source text for content support checking
|
|
354
|
+
base_result: The AssistResult before profile transformation
|
|
355
|
+
profiled_result: The AssistResult after profile transformation
|
|
356
|
+
ctx: Guard context with profile rules and constraints
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
GuardViolation: If any invariant is violated
|
|
360
|
+
"""
|
|
361
|
+
issues: List[GuardIssue] = []
|
|
362
|
+
|
|
363
|
+
# 1. Anchored ID invariant
|
|
364
|
+
_check_id_invariant(base_result, profiled_result, issues)
|
|
365
|
+
|
|
366
|
+
# 2. Confidence invariant
|
|
367
|
+
_check_confidence_invariant(base_result, profiled_result, issues)
|
|
368
|
+
|
|
369
|
+
# 3. Commands invariant
|
|
370
|
+
_check_commands_invariant(base_result, profiled_result, ctx, issues)
|
|
371
|
+
|
|
372
|
+
# 4. Step count invariant
|
|
373
|
+
_check_step_count_invariant(profiled_result, ctx, issues)
|
|
374
|
+
|
|
375
|
+
# 5. Content support invariant (WARN only)
|
|
376
|
+
_check_content_support_invariant(base_text, profiled_result, issues)
|
|
377
|
+
|
|
378
|
+
# 6. Profile-specific constraints
|
|
379
|
+
if ctx.forbid_parentheticals:
|
|
380
|
+
_check_parentheticals_constraint(profiled_result, issues)
|
|
381
|
+
|
|
382
|
+
if ctx.forbid_visual_refs:
|
|
383
|
+
_check_visual_refs_constraint(profiled_result, issues)
|
|
384
|
+
|
|
385
|
+
# Raise if any ERROR-level issues
|
|
386
|
+
errors = [i for i in issues if i.severity == "ERROR"]
|
|
387
|
+
if errors:
|
|
388
|
+
raise GuardViolation(errors)
|
|
389
|
+
|
|
390
|
+
# For WARN-level issues, we could log them but don't fail
|
|
391
|
+
# (In future, could add a strict mode that fails on WARN too)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Profile rules configuration
|
|
395
|
+
def get_guard_context(
|
|
396
|
+
profile: str,
|
|
397
|
+
confidence: Confidence,
|
|
398
|
+
input_kind: str,
|
|
399
|
+
allowed_commands: Set[str],
|
|
400
|
+
) -> GuardContext:
|
|
401
|
+
"""Create a GuardContext for a profile.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
profile: Profile name (lowvision, cognitive-load, screen-reader, dyslexia, plain-language)
|
|
405
|
+
confidence: Confidence level from base result
|
|
406
|
+
input_kind: Type of input (cli_error_json, raw_text, etc.)
|
|
407
|
+
allowed_commands: Set of allowed SAFE commands from base
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
GuardContext configured for the profile
|
|
411
|
+
"""
|
|
412
|
+
# Base configuration
|
|
413
|
+
forbid_parentheticals = False
|
|
414
|
+
forbid_visual_refs = False
|
|
415
|
+
max_steps: Optional[int] = None
|
|
416
|
+
allow_commands_on_low = False
|
|
417
|
+
|
|
418
|
+
if profile == "lowvision":
|
|
419
|
+
max_steps = 5
|
|
420
|
+
elif profile == "cognitive-load":
|
|
421
|
+
max_steps = 3
|
|
422
|
+
elif profile == "screen-reader":
|
|
423
|
+
forbid_parentheticals = True
|
|
424
|
+
forbid_visual_refs = True
|
|
425
|
+
# Screen-reader: 5 steps normally, 3 on Low confidence
|
|
426
|
+
max_steps = 3 if confidence == "Low" else 5
|
|
427
|
+
elif profile == "dyslexia":
|
|
428
|
+
forbid_parentheticals = True
|
|
429
|
+
forbid_visual_refs = True
|
|
430
|
+
max_steps = 5
|
|
431
|
+
elif profile == "plain-language":
|
|
432
|
+
forbid_parentheticals = True
|
|
433
|
+
max_steps = 4
|
|
434
|
+
|
|
435
|
+
return GuardContext(
|
|
436
|
+
profile=profile,
|
|
437
|
+
confidence=confidence,
|
|
438
|
+
input_kind=input_kind,
|
|
439
|
+
allowed_safe_commands=frozenset(allowed_commands),
|
|
440
|
+
forbid_parentheticals=forbid_parentheticals,
|
|
441
|
+
forbid_visual_refs=forbid_visual_refs,
|
|
442
|
+
max_steps=max_steps,
|
|
443
|
+
allow_commands_on_low=allow_commands_on_low,
|
|
444
|
+
)
|