plm-skill-kernel 1.0.2__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/PKG-INFO +3 -1
  2. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/__init__.py +1 -1
  3. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/__init__.py +84 -0
  4. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/_cli.py +143 -0
  5. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/_output_safety.py +55 -0
  6. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/_serialize.py +45 -0
  7. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/_terminal.py +70 -0
  8. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/_verdict.py +74 -0
  9. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/_version.py +7 -0
  10. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/exit_codes.py +23 -0
  11. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/models.py +301 -0
  12. plm_skill_kernel-1.1.0/plm_skill_kernel/conformance/protocol.py +46 -0
  13. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel.egg-info/PKG-INFO +3 -1
  14. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel.egg-info/SOURCES.txt +10 -0
  15. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel.egg-info/entry_points.txt +1 -0
  16. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel.egg-info/requires.txt +3 -0
  17. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/pyproject.toml +18 -1
  18. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/README.md +0 -0
  19. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/__main__.py +0 -0
  20. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/dispatcher.py +0 -0
  21. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/registry.py +0 -0
  22. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/sdk/__init__.py +0 -0
  23. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/sdk/_registry.py +0 -0
  24. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/sdk/context.py +0 -0
  25. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/sdk/decorator.py +0 -0
  26. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/sdk/types.py +0 -0
  27. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/server.py +0 -0
  28. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/__init__.py +0 -0
  29. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/agents/__init__.py +0 -0
  30. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/agents/suggest.py +0 -0
  31. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/bpmn/__init__.py +0 -0
  32. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/bpmn/generate.py +0 -0
  33. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/cleansing/__init__.py +0 -0
  34. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/cleansing/dedupe.py +0 -0
  35. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/cleansing/normalise.py +0 -0
  36. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/documents/__init__.py +0 -0
  37. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/documents/parse.py +0 -0
  38. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/__init__.py +0 -0
  39. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/_v1_corpus.py +0 -0
  40. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/get_record.py +0 -0
  41. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/resolve.py +0 -0
  42. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/retrieve.py +0 -0
  43. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/search.py +0 -0
  44. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills/knowledge/validate.py +0 -0
  45. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills_legacy/__init__.py +0 -0
  46. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills_legacy/knowledge_pack_loader.py +0 -0
  47. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills_legacy/knowledge_search_engine.py +0 -0
  48. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills_legacy/plm_skill_registry.py +0 -0
  49. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills_legacy/plm_tool_definitions.py +0 -0
  50. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel/skills_legacy/skill_orchestrator.py +0 -0
  51. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel.egg-info/dependency_links.txt +0 -0
  52. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/plm_skill_kernel.egg-info/top_level.txt +0 -0
  53. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/setup.cfg +0 -0
  54. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_agents_suggest_asgi.py +0 -0
  55. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_agents_suggest_unit.py +0 -0
  56. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_dispatcher.py +0 -0
  57. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_documents_parse_asgi.py +0 -0
  58. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_documents_parse_unit.py +0 -0
  59. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_sdk_decorator.py +0 -0
  60. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_server.py +0 -0
  61. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_skills_cleansing.py +0 -0
  62. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_skills_knowledge.py +0 -0
  63. {plm_skill_kernel-1.0.2 → plm_skill_kernel-1.1.0}/tests/test_skills_knowledge_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plm-skill-kernel
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: TracePulse PLM Skill Kernel — Wave 2 Conv A. Decorator-based Skill SDK + V1 in-process dispatcher + Kernel HTTP server stub matching CR.10 §7.bis (POST /v1/skills/{id}/invoke). Ships 3 starter skills (cleansing.normalise, cleansing.dedupe, bpmn.generate). Sibling of plm-engine-core; the two communicate over the V1.1 HTTP loopback (SKILL_KERNEL_LOOPBACK=on) — no Python-level coupling at the dispatch boundary.
5
5
  Author: TracePulse
6
6
  License: Proprietary
@@ -17,6 +17,8 @@ Provides-Extra: test
17
17
  Requires-Dist: pytest; extra == "test"
18
18
  Requires-Dist: pytest-asyncio; extra == "test"
19
19
  Requires-Dist: httpx<1,>=0.27; extra == "test"
20
+ Provides-Extra: conformance
21
+ Requires-Dist: plm-skill-packages; extra == "conformance"
20
22
  Provides-Extra: docs
21
23
  Requires-Dist: pdoc>=14.0; extra == "docs"
22
24
 
@@ -14,7 +14,7 @@ sanctioned interaction is the V1.1 HTTP loopback (env-flagged on
14
14
  ``SKILL_KERNEL_LOOPBACK``); any Python-level reach-in is forbidden by
15
15
  the import-linter contract in ``pyproject.toml``.
16
16
  """
17
- __version__ = "1.0.2"
17
+ __version__ = "1.1.0"
18
18
 
19
19
  # The decorator + sdk surface is the public authoring API. Re-exported
20
20
  # here so callers write ``from plm_skill_kernel import skill`` rather
@@ -0,0 +1,84 @@
1
+ """Conformance contract layer (FTR-2008 / MV1-DEC-018 Candidate 1).
2
+
3
+ Owns:
4
+ - :class:`ManifestProfileValidator` typing.Protocol (the validator seam)
5
+ - :class:`ConformanceReport` + :class:`ConformanceFinding` (report models)
6
+ - :data:`REPORT_CONTRACT_VERSION` (independent SemVer per MV1-DEC-005)
7
+ - :class:`ExitCode` constants (MV1-DEC-006)
8
+
9
+ Does NOT extend or import the 4 frozen SDK wire models
10
+ (SkillContext, SkillInvokeRequest, SkillInvokeResult, SkillErrorEnvelope).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Dict, List, Type
15
+
16
+ from ._version import REPORT_CONTRACT_VERSION
17
+ from .exit_codes import ExitCode
18
+ from .models import (
19
+ ConformanceFinding,
20
+ ConformanceReport,
21
+ EnumerationEntry,
22
+ EnumerationPayload,
23
+ EnumerationState,
24
+ ExecutionStatusCounts,
25
+ FindingExecutionStatus,
26
+ FindingSeverity,
27
+ ResolvedReference,
28
+ SeverityCounts,
29
+ Verdict,
30
+ )
31
+ from .protocol import ManifestProfileValidator
32
+ from ._verdict import derive_exit_code, expected_exit_code
33
+
34
+ __all__ = [
35
+ "REPORT_CONTRACT_VERSION",
36
+ "ConformanceFinding",
37
+ "ConformanceReport",
38
+ "EnumerationEntry",
39
+ "EnumerationPayload",
40
+ "EnumerationState",
41
+ "ExitCode",
42
+ "ExecutionStatusCounts",
43
+ "FindingExecutionStatus",
44
+ "FindingSeverity",
45
+ "ManifestProfileValidator",
46
+ "ResolvedReference",
47
+ "SeverityCounts",
48
+ "Verdict",
49
+ "available_profiles",
50
+ "derive_exit_code",
51
+ "expected_exit_code",
52
+ "get_validator",
53
+ "register_profile",
54
+ ]
55
+
56
+
57
+ # ── Profile registration seam ────────────────────────────────────────────────
58
+
59
+ _PROFILE_REGISTRY: Dict[str, Type[ManifestProfileValidator]] = {}
60
+
61
+
62
+ def register_profile(
63
+ profile_id: str, validator_cls: Type[ManifestProfileValidator]
64
+ ) -> None:
65
+ """Register a validator class for a profile ID.
66
+
67
+ Called by profile implementations (e.g. in plm-skill-packages) at
68
+ import time to make themselves available to the CLI.
69
+ """
70
+ _PROFILE_REGISTRY[profile_id] = validator_cls
71
+
72
+
73
+ def get_validator(profile_id: str) -> ManifestProfileValidator:
74
+ """Instantiate and return the validator for *profile_id*.
75
+
76
+ Raises :class:`KeyError` if *profile_id* is not registered.
77
+ """
78
+ cls = _PROFILE_REGISTRY[profile_id]
79
+ return cls()
80
+
81
+
82
+ def available_profiles() -> List[str]:
83
+ """Return list of registered profile IDs."""
84
+ return sorted(_PROFILE_REGISTRY.keys())
@@ -0,0 +1,143 @@
1
+ """CLI entry for ``plm-skill-conformance`` — conformance validation runner.
2
+
3
+ Usage::
4
+
5
+ plm-skill-conformance --profile manifest-v1 [--report <path>] [snapshot_root]
6
+
7
+ Exit codes per MV1-DEC-006:
8
+ 0 = VALID
9
+ 1 = INVALID (blocking findings)
10
+ 2 = decision-gated / not-executable / degraded
11
+ 64 = invoker-correctable (usage/config/profile/unsafe-path)
12
+ 70 = unexpected defect
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+ from pathlib import Path
19
+ from typing import List, Optional
20
+
21
+ from ._output_safety import validate_output_path
22
+ from ._serialize import serialize_report
23
+ from ._terminal import render_terminal
24
+ from .exit_codes import ExitCode
25
+ from .models import ConformanceReport
26
+
27
+
28
+ def main(argv: Optional[List[str]] = None) -> None:
29
+ """Console-script entry — ``plm-skill-conformance``.
30
+
31
+ Separate from the server CLI (``plm-skill-kernel``) to satisfy AC10:
32
+ running without --profile on the server CLI remains unchanged.
33
+ """
34
+ parser = argparse.ArgumentParser(
35
+ prog="plm-skill-conformance",
36
+ description="Run manifest conformance validation against a profile.",
37
+ )
38
+ parser.add_argument(
39
+ "--profile",
40
+ required=False,
41
+ default=None,
42
+ help="Profile to validate against (e.g. 'manifest-v1'). Required.",
43
+ )
44
+ parser.add_argument(
45
+ "--report",
46
+ type=str,
47
+ default=None,
48
+ help=(
49
+ "Output path for the JSON report. "
50
+ "Default: <snapshot_root>/.plm/skill-conformance/report.json"
51
+ ),
52
+ )
53
+ parser.add_argument(
54
+ "snapshot_root",
55
+ nargs="?",
56
+ default=".",
57
+ help="Path to the package snapshot root (default: current directory).",
58
+ )
59
+
60
+ args = parser.parse_args(argv)
61
+
62
+ # MV1-DEC-006: missing --profile is invoker-correctable (exit 64)
63
+ if args.profile is None:
64
+ print("ERROR: --profile is required.", file=sys.stderr)
65
+ sys.exit(ExitCode.INVOKER_CORRECTABLE)
66
+
67
+ # Resolve snapshot root
68
+ snapshot_root = Path(args.snapshot_root).resolve()
69
+
70
+ # Determine report output path
71
+ if args.report:
72
+ report_path = Path(args.report).resolve()
73
+ else:
74
+ report_path = snapshot_root / ".plm" / "skill-conformance" / "report.json"
75
+
76
+ # Output-path safety check (AC7, AC8, AC14) — BEFORE any validation
77
+ safety_error = validate_output_path(report_path, snapshot_root)
78
+ if safety_error:
79
+ print(f"ERROR: {safety_error}", file=sys.stderr)
80
+ sys.exit(ExitCode.INVOKER_CORRECTABLE)
81
+
82
+ # Load the profile validator via registration seam
83
+ try:
84
+ from . import available_profiles, get_validator
85
+
86
+ validator = get_validator(args.profile)
87
+ except KeyError:
88
+ from . import available_profiles
89
+
90
+ print(
91
+ f"ERROR: Unknown profile '{args.profile}'. "
92
+ f"Available: {available_profiles()}",
93
+ file=sys.stderr,
94
+ )
95
+ sys.exit(ExitCode.INVOKER_CORRECTABLE)
96
+ except ImportError as exc:
97
+ print(
98
+ f"ERROR: Failed to load profile validator: {exc}. "
99
+ f"Install plm-skill-packages or the appropriate validator package.",
100
+ file=sys.stderr,
101
+ )
102
+ sys.exit(ExitCode.INVOKER_CORRECTABLE)
103
+ except Exception as exc:
104
+ print(
105
+ f"ERROR: Failed to instantiate profile validator: {exc}",
106
+ file=sys.stderr,
107
+ )
108
+ sys.exit(ExitCode.INVOKER_CORRECTABLE)
109
+
110
+ # Run validation — fail-closed: exceptions → exit 70
111
+ try:
112
+ report = validator.validate(str(snapshot_root))
113
+ except Exception as exc:
114
+ print(
115
+ f"DEFECT: Validator raised unexpected error: {type(exc).__name__}: {exc}",
116
+ file=sys.stderr,
117
+ )
118
+ sys.exit(ExitCode.DEFECT)
119
+
120
+ # Canonicalize for byte-identity (AC8/AC13)
121
+ canonical_report = ConformanceReport.canonicalize(report)
122
+
123
+ # Write JSON report — fail-closed: exceptions → exit 70
124
+ try:
125
+ report_path.parent.mkdir(parents=True, exist_ok=True)
126
+ report_path.write_text(
127
+ serialize_report(canonical_report), encoding="utf-8"
128
+ )
129
+ except Exception as exc:
130
+ print(
131
+ f"DEFECT: Failed to write report to '{report_path}': {exc}",
132
+ file=sys.stderr,
133
+ )
134
+ sys.exit(ExitCode.DEFECT)
135
+
136
+ # Terminal projection
137
+ render_terminal(canonical_report)
138
+
139
+ # Exit with the guaranteed-consistent exit code from the report
140
+ sys.exit(canonical_report.exit_code)
141
+
142
+
143
+ __all__ = ["main"]
@@ -0,0 +1,55 @@
1
+ """Output-path safety validation (AC7/AC8/AC14).
2
+
3
+ Rules:
4
+ - Report path OUTSIDE snapshot root → always safe.
5
+ - Report path INSIDE snapshot root under a pre-excluded directory → safe.
6
+ - Report path INSIDE snapshot root, NOT under a pre-excluded directory →
7
+ FAIL before writing (exit 64, invoker-correctable).
8
+
9
+ The default report destination is ``<snapshot_root>/.plm/skill-conformance/report.json``
10
+ which is inside the snapshot but under ``.plm/`` (pre-excluded), so it is safe.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ # Directories that are pre-excluded from package enumeration — safe to write into.
18
+ PRE_EXCLUDED_DIRS = frozenset({
19
+ ".plm", ".git", ".federation", "__pycache__", "node_modules",
20
+ ".venv", "venv", ".tox", ".mypy_cache", ".pytest_cache",
21
+ })
22
+
23
+
24
+ def validate_output_path(report_path: Path, snapshot_root: Path) -> Optional[str]:
25
+ """Return an error message if *report_path* is unsafe; ``None`` if OK.
26
+
27
+ An unsafe path is one that is inside the snapshot root but not under
28
+ a pre-excluded directory — writing there would contaminate the
29
+ authoring snapshot and potentially alter enumeration results.
30
+ """
31
+ # Resolve both to absolute for reliable comparison
32
+ report_abs = report_path.resolve()
33
+ snapshot_abs = snapshot_root.resolve()
34
+
35
+ # Outside snapshot → always safe
36
+ try:
37
+ relative = report_abs.relative_to(snapshot_abs)
38
+ except ValueError:
39
+ return None
40
+
41
+ # Inside snapshot — check if first path component is pre-excluded
42
+ parts = relative.parts
43
+ if parts and parts[0] in PRE_EXCLUDED_DIRS:
44
+ return None
45
+
46
+ return (
47
+ f"Report path '{report_abs}' is inside the snapshot root "
48
+ f"'{snapshot_abs}' and not under a pre-excluded directory "
49
+ f"({sorted(PRE_EXCLUDED_DIRS)}). Writing here would contaminate "
50
+ f"the snapshot. Use --report with a path outside the snapshot "
51
+ f"or under .plm/."
52
+ )
53
+
54
+
55
+ __all__ = ["PRE_EXCLUDED_DIRS", "validate_output_path"]
@@ -0,0 +1,45 @@
1
+ """Canonical report serialization (AC8/AC13 byte-identity guarantee).
2
+
3
+ This module is the SINGLE serialization boundary for conformance reports.
4
+ All code that produces report JSON bytes MUST use :func:`serialize_report`
5
+ rather than calling ``model_dump_json()`` directly.
6
+
7
+ Key guarantee: ``ensure_ascii=True`` ensures non-ASCII characters are
8
+ escaped as ``\\uXXXX``, making the output byte-identical across platforms
9
+ regardless of locale or filesystem encoding.
10
+
11
+ Added in REPORT_CONTRACT_VERSION 1.1.0 (MV1-DEC-005 SemVer-minor).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+
17
+ from .models import ConformanceReport
18
+
19
+
20
+ def serialize_report(report: ConformanceReport) -> str:
21
+ """Produce canonical JSON for a conformance report.
22
+
23
+ Round-trips through Pydantic's serializer (for type coercion and
24
+ field-order stability) then re-serializes with ``ensure_ascii=True``
25
+ for cross-platform byte-identity.
26
+
27
+ Parameters
28
+ ----------
29
+ report : ConformanceReport
30
+ Should already be canonicalized via ``ConformanceReport.canonicalize()``.
31
+
32
+ Returns
33
+ -------
34
+ str
35
+ Canonical JSON string (indent=2, ensure_ascii=True, no trailing newline).
36
+ """
37
+ return json.dumps(
38
+ json.loads(report.model_dump_json()),
39
+ indent=2,
40
+ ensure_ascii=True,
41
+ sort_keys=False,
42
+ )
43
+
44
+
45
+ __all__ = ["serialize_report"]
@@ -0,0 +1,70 @@
1
+ """Deterministic terminal projection (MV1-DEC-005).
2
+
3
+ Order: verdict line → profile/version → report contract version → counts →
4
+ findings sorted by (severity DESC [error>warning>info], rule_code ASC, path ASC).
5
+
6
+ Same report → identical terminal output.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import IO
12
+
13
+ from .models import (
14
+ ConformanceFinding,
15
+ ConformanceReport,
16
+ FindingSeverity,
17
+ )
18
+
19
+
20
+ _SEVERITY_ORDER = {
21
+ FindingSeverity.ERROR: 0,
22
+ FindingSeverity.WARNING: 1,
23
+ FindingSeverity.INFO: 2,
24
+ }
25
+
26
+
27
+ def render_terminal(report: ConformanceReport, *, stream: IO[str] = sys.stdout) -> None:
28
+ """Render the conformance report to terminal in deterministic order.
29
+
30
+ Parameters
31
+ ----------
32
+ report : ConformanceReport
33
+ The canonicalized report to render.
34
+ stream : IO[str]
35
+ Output stream (default: stdout). Overridable for testing.
36
+ """
37
+ # Verdict line
38
+ stream.write(f"\nVerdict: {report.verdict.value}\n")
39
+ stream.write(f"Profile: {report.profile_id} ({report.profile_version})\n")
40
+ stream.write(f"Report contract: {report.report_contract_version}\n")
41
+
42
+ # Counts
43
+ c = report.counts_by_severity
44
+ stream.write(
45
+ f"\nFindings: {c.error} error, {c.warning} warning, {c.info} info\n"
46
+ )
47
+
48
+ # Deterministically ordered findings
49
+ if report.findings:
50
+ ordered = sorted(
51
+ report.findings,
52
+ key=lambda f: (
53
+ _SEVERITY_ORDER.get(f.severity, 99),
54
+ f.rule_code,
55
+ f.path or "",
56
+ ),
57
+ )
58
+ stream.write("\n--- Findings ---\n")
59
+ for f in ordered:
60
+ path_str = f" @ {f.path}" if f.path else ""
61
+ decision_str = f" [decision: {f.decision_id}]" if f.decision_id else ""
62
+ stream.write(
63
+ f" [{f.severity.value.upper()}] "
64
+ f"{f.rule_code}{path_str}: {f.message}{decision_str}\n"
65
+ )
66
+
67
+ stream.write("\n")
68
+
69
+
70
+ __all__ = ["render_terminal"]
@@ -0,0 +1,74 @@
1
+ """Verdict-to-exit-code derivation (MV1-DEC-006).
2
+
3
+ Deterministic verdict precedence (highest → lowest):
4
+
5
+ 1. INVALID (exit 1) — any finding with severity=error + execution_status=invalid
6
+ 2. decision-gated (exit 2) — any finding with execution_status=blocked_by_decision
7
+ 3. required-not-executable (exit 2) — execution_status=not_executable
8
+ 4. compatibility-not-executable (exit 2) — execution_status=compatibility_not_executable
9
+ 5. degraded-enumeration (exit 2) — enumeration.state == DEGRADED
10
+ 6. VALID (exit 0) — none of the above
11
+
12
+ When INVALID and gated/not-executable co-exist, INVALID wins (exit 1 > exit 2).
13
+ Rationale: blocking conformance failures are author-actionable NOW (the author
14
+ can fix the schema error); gated states depend on external resolution (a decision
15
+ owner must close the decision, or a dependency must be published). Reporting
16
+ INVALID is the conservative/safe outcome — it surfaces the actionable fix first.
17
+
18
+ The precedence is enforced by the *validator* when it produces the verdict.
19
+ The kernel-side ``derive_exit_code`` simply maps verdict → exit code and
20
+ returns 70 (defect) for any unknown/future verdict value (fail-closed).
21
+
22
+ PENDING RATIFICATION (FTR-2034 decision register)
23
+ ──────────────────────────────────────────────────
24
+ This verdict-precedence rule (INVALID > gated when both coexist) is a proposed
25
+ kernel-side consistency rule. It is NOT yet a ratified MV1-DEC-* decision.
26
+ It is locked by regression tests in tests/conformance/test_verdict_derivation.py
27
+ (test_invalid_wins_over_gated, test_precedence_invalid_over_not_executable).
28
+ Any change to this ordering requires explicit owner ratification via FTR-2034.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import TYPE_CHECKING
35
+
36
+ from .exit_codes import ExitCode
37
+ from .models import Verdict
38
+
39
+ if TYPE_CHECKING:
40
+ from .models import ConformanceReport
41
+
42
+
43
+ _VERDICT_TO_EXIT: dict[Verdict, int] = {
44
+ Verdict.VALID: ExitCode.VALID,
45
+ Verdict.INVALID: ExitCode.INVALID,
46
+ Verdict.DECISION_GATED: ExitCode.GATED,
47
+ Verdict.REQUIRED_NOT_EXECUTABLE: ExitCode.GATED,
48
+ Verdict.COMPATIBILITY_NOT_EXECUTABLE: ExitCode.GATED,
49
+ Verdict.DEGRADED_ENUMERATION: ExitCode.GATED,
50
+ }
51
+
52
+
53
+ def expected_exit_code(verdict: Verdict) -> int:
54
+ """Single authority for verdict → exit-code mapping (MV1-DEC-006).
55
+
56
+ Pure function: takes only a Verdict enum, never reads report.exit_code.
57
+ Used by the model validator to enforce consistency and by all report
58
+ producers to compute the correct exit_code before construction.
59
+
60
+ Fail-closed: unknown verdict value returns 70 (defect).
61
+ """
62
+ return _VERDICT_TO_EXIT.get(verdict, ExitCode.DEFECT)
63
+
64
+
65
+ def derive_exit_code(report: "ConformanceReport") -> int:
66
+ """Map report verdict to exit code — thin wrapper over expected_exit_code.
67
+
68
+ Kept for backward compatibility. Delegates entirely to
69
+ :func:`expected_exit_code`.
70
+ """
71
+ return expected_exit_code(report.verdict)
72
+
73
+
74
+ __all__ = ["derive_exit_code", "expected_exit_code"]
@@ -0,0 +1,7 @@
1
+ """Report contract version — independently versioned per MV1-DEC-005.
2
+
3
+ SemVer rules:
4
+ - Minor bump: add optional field to ConformanceReport or ConformanceFinding.
5
+ - Major bump: remove/rename/require a field, or change verdict semantics.
6
+ """
7
+ REPORT_CONTRACT_VERSION = "1.1.0"
@@ -0,0 +1,23 @@
1
+ """Exit-code constants per MV1-DEC-006.
2
+
3
+ Mapping:
4
+ 0 — VALID (conformant)
5
+ 1 — INVALID (blocking conformance findings, severity=error)
6
+ 2 — decision-gated / required-not-executable /
7
+ compatibility-not-executable / degraded-enumeration
8
+ 64 — invoker-correctable (usage, config, profile selection, unsafe path)
9
+ 70 — unexpected defect (validator crash, serialization failure, write error)
10
+ """
11
+
12
+
13
+ class ExitCode:
14
+ """MV1-DEC-006 exit codes for conformance validation."""
15
+
16
+ VALID = 0
17
+ INVALID = 1
18
+ GATED = 2
19
+ INVOKER_CORRECTABLE = 64
20
+ DEFECT = 70
21
+
22
+
23
+ __all__ = ["ExitCode"]
@@ -0,0 +1,301 @@
1
+ """Conformance report and finding models (MV1-DEC-005 / MV1-DEC-018).
2
+
3
+ All models are frozen Pydantic v2 with ``extra="forbid"`` — the same
4
+ pattern used by the SDK wire shapes, but these live in a separate
5
+ namespace and are independently versioned by ``REPORT_CONTRACT_VERSION``.
6
+
7
+ The ``ConformanceReport`` carries a ``model_validator`` that enforces
8
+ the AC9 fail-closed invariant: verdict=VALID is rejected if the report
9
+ contains blocking findings, decision-gated / not-executable states, or
10
+ degraded enumeration.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from enum import Enum
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
18
+
19
+ from ._version import REPORT_CONTRACT_VERSION
20
+
21
+
22
+ # ── Enumerations ─────────────────────────────────────────────────────────────
23
+
24
+
25
+ class Verdict(str, Enum):
26
+ """Overall conformance verdict."""
27
+
28
+ VALID = "VALID"
29
+ INVALID = "INVALID"
30
+ DECISION_GATED = "decision-gated"
31
+ REQUIRED_NOT_EXECUTABLE = "required-not-executable"
32
+ COMPATIBILITY_NOT_EXECUTABLE = "compatibility-not-executable"
33
+ DEGRADED_ENUMERATION = "degraded-enumeration"
34
+
35
+
36
+ class FindingSeverity(str, Enum):
37
+ """Finding severity level."""
38
+
39
+ ERROR = "error"
40
+ WARNING = "warning"
41
+ INFO = "info"
42
+
43
+
44
+ class FindingExecutionStatus(str, Enum):
45
+ """Per-finding execution status."""
46
+
47
+ VALID = "valid"
48
+ INVALID = "invalid"
49
+ BLOCKED_BY_DECISION = "blocked_by_decision"
50
+ COMPATIBILITY_NOT_EXECUTABLE = "compatibility_not_executable"
51
+ NOT_EXECUTABLE = "not_executable"
52
+
53
+
54
+ class EnumerationState(str, Enum):
55
+ """Enumeration completeness state."""
56
+
57
+ COMPLETE = "complete"
58
+ DEGRADED = "degraded"
59
+
60
+
61
+ # ── Finding model ────────────────────────────────────────────────────────────
62
+
63
+
64
+ class ConformanceFinding(BaseModel):
65
+ """Single conformance finding — immutable, deterministically serializable."""
66
+
67
+ model_config = ConfigDict(frozen=True, extra="forbid")
68
+
69
+ rule_code: str = Field(min_length=1)
70
+ severity: FindingSeverity
71
+ path: Optional[str] = None
72
+ message: str = Field(min_length=1)
73
+ remediation: Optional[str] = None
74
+ decision_id: Optional[str] = None
75
+ execution_status: FindingExecutionStatus
76
+
77
+
78
+ # ── Enumeration models ───────────────────────────────────────────────────────
79
+
80
+
81
+ class EnumerationEntry(BaseModel):
82
+ """Single entry in the enumeration payload."""
83
+
84
+ model_config = ConfigDict(frozen=True, extra="forbid")
85
+
86
+ relative_path: str
87
+ ordering_key: str
88
+ entry_state: str
89
+ reference_state: Optional[str] = None
90
+
91
+
92
+ class EnumerationPayload(BaseModel):
93
+ """Enumeration section of the conformance report."""
94
+
95
+ model_config = ConfigDict(frozen=True, extra="forbid")
96
+
97
+ state: EnumerationState
98
+ causes: List[str] = Field(default_factory=list)
99
+ entries: List[EnumerationEntry] = Field(default_factory=list)
100
+
101
+
102
+ # ── Reference inventory ──────────────────────────────────────────────────────
103
+
104
+
105
+ class ResolvedReference(BaseModel):
106
+ """Single entry in the resolved-reference inventory."""
107
+
108
+ model_config = ConfigDict(frozen=True, extra="forbid")
109
+
110
+ reference_id: str
111
+ resolved_path: Optional[str] = None
112
+ status: str
113
+
114
+
115
+ # ── Count models ─────────────────────────────────────────────────────────────
116
+
117
+
118
+ class SeverityCounts(BaseModel):
119
+ """Finding counts by severity."""
120
+
121
+ model_config = ConfigDict(frozen=True, extra="forbid")
122
+
123
+ error: int = 0
124
+ warning: int = 0
125
+ info: int = 0
126
+
127
+
128
+ class ExecutionStatusCounts(BaseModel):
129
+ """Finding counts by execution status."""
130
+
131
+ model_config = ConfigDict(frozen=True, extra="forbid")
132
+
133
+ valid: int = 0
134
+ invalid: int = 0
135
+ blocked_by_decision: int = 0
136
+ compatibility_not_executable: int = 0
137
+ not_executable: int = 0
138
+
139
+
140
+ # ── Top-level report ─────────────────────────────────────────────────────────
141
+
142
+
143
+ _SEVERITY_RANK = {
144
+ FindingSeverity.ERROR: 0,
145
+ FindingSeverity.WARNING: 1,
146
+ FindingSeverity.INFO: 2,
147
+ }
148
+
149
+ # Execution statuses that make VALID impossible (AC9).
150
+ _BLOCKING_EXECUTION_STATUSES = frozenset({
151
+ FindingExecutionStatus.BLOCKED_BY_DECISION,
152
+ FindingExecutionStatus.NOT_EXECUTABLE,
153
+ FindingExecutionStatus.COMPATIBILITY_NOT_EXECUTABLE,
154
+ })
155
+
156
+
157
+ class ConformanceReport(BaseModel):
158
+ """Top-level conformance report — the canonical machine-readable output.
159
+
160
+ Versioned by ``report_contract_version`` (SemVer per MV1-DEC-005).
161
+ Adding optional fields is a minor bump; removing/renaming/requiring
162
+ a field or changing verdict semantics is a major bump.
163
+ """
164
+
165
+ model_config = ConfigDict(frozen=True, extra="forbid")
166
+
167
+ report_contract_version: str = Field(default=REPORT_CONTRACT_VERSION)
168
+ profile_id: str
169
+ profile_version: str
170
+ schema_version: Optional[str] = None
171
+ rule_versions: Optional[Dict[str, str]] = None
172
+
173
+ verdict: Verdict
174
+ exit_code: int
175
+
176
+ findings: List[ConformanceFinding] = Field(default_factory=list)
177
+ counts_by_severity: SeverityCounts = Field(default_factory=SeverityCounts)
178
+ counts_by_execution_status: ExecutionStatusCounts = Field(
179
+ default_factory=ExecutionStatusCounts
180
+ )
181
+
182
+ enumeration: Optional[EnumerationPayload] = None
183
+ resolved_references: List[ResolvedReference] = Field(default_factory=list)
184
+ migration_assessment: Optional[Dict[str, Any]] = None
185
+
186
+ @model_validator(mode="after")
187
+ def _ac9_fail_closed_guard(self) -> "ConformanceReport":
188
+ """Reject VALID verdict when blocking content exists (AC9).
189
+
190
+ The Kernel does NOT trust the validator's verdict blindly. If any
191
+ of these conditions hold, VALID is impossible and construction
192
+ raises ``ValidationError``.
193
+ """
194
+ if self.verdict != Verdict.VALID:
195
+ return self
196
+
197
+ # Check blocking findings (severity=error)
198
+ has_blocking = any(
199
+ f.severity == FindingSeverity.ERROR for f in self.findings
200
+ )
201
+ if has_blocking:
202
+ raise ValueError(
203
+ "verdict=VALID is impossible: report contains error-severity findings"
204
+ )
205
+
206
+ # Check decision-gated / not-executable states
207
+ has_gated = any(
208
+ f.execution_status in _BLOCKING_EXECUTION_STATUSES
209
+ for f in self.findings
210
+ )
211
+ if has_gated:
212
+ raise ValueError(
213
+ "verdict=VALID is impossible: report contains "
214
+ "decision-gated or not-executable findings"
215
+ )
216
+
217
+ # Check degraded enumeration
218
+ if self.enumeration and self.enumeration.state == EnumerationState.DEGRADED:
219
+ raise ValueError(
220
+ "verdict=VALID is impossible: enumeration is degraded"
221
+ )
222
+
223
+ return self
224
+
225
+ @model_validator(mode="after")
226
+ def _exit_code_consistency_guard(self) -> "ConformanceReport":
227
+ """Reject reports where exit_code disagrees with verdict.
228
+
229
+ Uses :func:`expected_exit_code` (the single authority) which
230
+ derives from verdict alone — never reads self.exit_code, so
231
+ this guard is not a tautology.
232
+ """
233
+ from ._verdict import expected_exit_code # deferred to avoid circular
234
+
235
+ expected = expected_exit_code(self.verdict)
236
+ if self.exit_code != expected:
237
+ raise ValueError(
238
+ f"exit_code={self.exit_code} is inconsistent with "
239
+ f"verdict={self.verdict.value} (expected {expected})"
240
+ )
241
+ return self
242
+
243
+ @classmethod
244
+ def canonicalize(cls, report: "ConformanceReport") -> "ConformanceReport":
245
+ """Return a new instance with all lists in deterministic order.
246
+
247
+ Sort keys:
248
+ - findings: (severity_rank ASC, rule_code ASC, path ASC)
249
+ - resolved_references: (reference_id ASC)
250
+ - enumeration.entries: (ordering_key ASC)
251
+
252
+ Call before serialization to guarantee byte-identity (AC8/AC13).
253
+ """
254
+ sorted_findings = sorted(
255
+ report.findings,
256
+ key=lambda f: (
257
+ _SEVERITY_RANK.get(f.severity, 99),
258
+ f.rule_code,
259
+ f.path or "",
260
+ ),
261
+ )
262
+ sorted_refs = sorted(
263
+ report.resolved_references,
264
+ key=lambda r: r.reference_id,
265
+ )
266
+ sorted_enum = None
267
+ if report.enumeration:
268
+ sorted_entries = sorted(
269
+ report.enumeration.entries,
270
+ key=lambda e: e.ordering_key,
271
+ )
272
+ sorted_enum = EnumerationPayload(
273
+ state=report.enumeration.state,
274
+ causes=list(report.enumeration.causes),
275
+ entries=sorted_entries,
276
+ )
277
+
278
+ return cls.model_validate(
279
+ report.model_dump() | {
280
+ "findings": [f.model_dump() for f in sorted_findings],
281
+ "resolved_references": [r.model_dump() for r in sorted_refs],
282
+ "enumeration": (
283
+ sorted_enum.model_dump() if sorted_enum else None
284
+ ),
285
+ }
286
+ )
287
+
288
+
289
+ __all__ = [
290
+ "ConformanceFinding",
291
+ "ConformanceReport",
292
+ "EnumerationEntry",
293
+ "EnumerationPayload",
294
+ "EnumerationState",
295
+ "ExecutionStatusCounts",
296
+ "FindingExecutionStatus",
297
+ "FindingSeverity",
298
+ "ResolvedReference",
299
+ "SeverityCounts",
300
+ "Verdict",
301
+ ]
@@ -0,0 +1,46 @@
1
+ """ManifestProfileValidator typing.Protocol (MV1-DEC-018 Candidate 1).
2
+
3
+ The Kernel owns this Protocol; implementations live in plm-skill-packages
4
+ or are injected at runtime via the registration seam in ``__init__.py``.
5
+
6
+ The Protocol is intentionally minimal — it accepts a snapshot root path
7
+ and returns a :class:`ConformanceReport`. Validators MUST be fail-closed:
8
+ internal errors produce a defect-level finding rather than silently passing.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
13
+
14
+ if TYPE_CHECKING:
15
+ from .models import ConformanceReport
16
+
17
+
18
+ @runtime_checkable
19
+ class ManifestProfileValidator(Protocol):
20
+ """Contract that any profile validator implementation must satisfy.
21
+
22
+ Implementations register via
23
+ :func:`plm_skill_kernel.conformance.register_profile`.
24
+ """
25
+
26
+ @property
27
+ def profile_id(self) -> str:
28
+ """Canonical profile identifier, e.g. ``'manifest-v1'``."""
29
+ ...
30
+
31
+ @property
32
+ def profile_version(self) -> str:
33
+ """SemVer of the profile definition (e.g. ``'manifest-profile/1.0'``)."""
34
+ ...
35
+
36
+ def validate(self, snapshot_root: str) -> "ConformanceReport":
37
+ """Run full conformance validation against *snapshot_root*.
38
+
39
+ MUST be fail-closed: any internal error produces a defect-level
40
+ finding rather than silently passing. Never raises — all failures
41
+ are expressed as findings within the returned report.
42
+ """
43
+ ...
44
+
45
+
46
+ __all__ = ["ManifestProfileValidator"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plm-skill-kernel
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: TracePulse PLM Skill Kernel — Wave 2 Conv A. Decorator-based Skill SDK + V1 in-process dispatcher + Kernel HTTP server stub matching CR.10 §7.bis (POST /v1/skills/{id}/invoke). Ships 3 starter skills (cleansing.normalise, cleansing.dedupe, bpmn.generate). Sibling of plm-engine-core; the two communicate over the V1.1 HTTP loopback (SKILL_KERNEL_LOOPBACK=on) — no Python-level coupling at the dispatch boundary.
5
5
  Author: TracePulse
6
6
  License: Proprietary
@@ -17,6 +17,8 @@ Provides-Extra: test
17
17
  Requires-Dist: pytest; extra == "test"
18
18
  Requires-Dist: pytest-asyncio; extra == "test"
19
19
  Requires-Dist: httpx<1,>=0.27; extra == "test"
20
+ Provides-Extra: conformance
21
+ Requires-Dist: plm-skill-packages; extra == "conformance"
20
22
  Provides-Extra: docs
21
23
  Requires-Dist: pdoc>=14.0; extra == "docs"
22
24
 
@@ -11,6 +11,16 @@ plm_skill_kernel.egg-info/dependency_links.txt
11
11
  plm_skill_kernel.egg-info/entry_points.txt
12
12
  plm_skill_kernel.egg-info/requires.txt
13
13
  plm_skill_kernel.egg-info/top_level.txt
14
+ plm_skill_kernel/conformance/__init__.py
15
+ plm_skill_kernel/conformance/_cli.py
16
+ plm_skill_kernel/conformance/_output_safety.py
17
+ plm_skill_kernel/conformance/_serialize.py
18
+ plm_skill_kernel/conformance/_terminal.py
19
+ plm_skill_kernel/conformance/_verdict.py
20
+ plm_skill_kernel/conformance/_version.py
21
+ plm_skill_kernel/conformance/exit_codes.py
22
+ plm_skill_kernel/conformance/models.py
23
+ plm_skill_kernel/conformance/protocol.py
14
24
  plm_skill_kernel/sdk/__init__.py
15
25
  plm_skill_kernel/sdk/_registry.py
16
26
  plm_skill_kernel/sdk/context.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ plm-skill-conformance = plm_skill_kernel.conformance._cli:main
2
3
  plm-skill-kernel = plm_skill_kernel.server:_cli_entry
@@ -6,6 +6,9 @@ httpx>=0.27.0
6
6
  plm-engine-core
7
7
  plm-knowledge
8
8
 
9
+ [conformance]
10
+ plm-skill-packages
11
+
9
12
  [docs]
10
13
  pdoc>=14.0
11
14
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "plm-skill-kernel"
7
- version = "1.0.2"
7
+ version = "1.1.0"
8
8
  description = "TracePulse PLM Skill Kernel — Wave 2 Conv A. Decorator-based Skill SDK + V1 in-process dispatcher + Kernel HTTP server stub matching CR.10 §7.bis (POST /v1/skills/{id}/invoke). Ships 3 starter skills (cleansing.normalise, cleansing.dedupe, bpmn.generate). Sibling of plm-engine-core; the two communicate over the V1.1 HTTP loopback (SKILL_KERNEL_LOOPBACK=on) — no Python-level coupling at the dispatch boundary."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -44,6 +44,12 @@ test = [
44
44
  # HTTP loopback integration test (Q-W2A-4 = ASGI).
45
45
  "httpx>=0.27,<1",
46
46
  ]
47
+ conformance = [
48
+ # FTR-2008 — the conformance CLI needs plm-skill-packages for the
49
+ # manifest-v1 profile validator implementation. Optional because the
50
+ # Protocol/models are testable standalone.
51
+ "plm-skill-packages",
52
+ ]
47
53
  docs = [
48
54
  # pdoc-based API reference site, built in CI on every push.
49
55
  # Ported from plm-shared (FTR-774 follow-up). See ci.yml::docs-build.
@@ -55,6 +61,10 @@ docs = [
55
61
  # `python -m plm_skill_kernel.server`. Default port 8100 keeps
56
62
  # clear of the backend at 8000.
57
63
  plm-skill-kernel = "plm_skill_kernel.server:_cli_entry"
64
+ # FTR-2008 — conformance validation CLI (MV1-DEC-018 Candidate 1).
65
+ # Separate entry from the server CLI to satisfy AC10 (unchanged behavior
66
+ # when --profile is absent on the server CLI).
67
+ plm-skill-conformance = "plm_skill_kernel.conformance._cli:main"
58
68
 
59
69
  [tool.setuptools.packages.find]
60
70
  where = ["."]
@@ -116,3 +126,10 @@ type = "forbidden"
116
126
  source_modules = ["plm_skill_kernel"]
117
127
  forbidden_modules = ["plm_accelerators"]
118
128
  ignore_imports = []
129
+
130
+ [[tool.importlinter.contracts]]
131
+ name = "conformance-must-not-import-sdk-wire-models"
132
+ type = "forbidden"
133
+ source_modules = ["plm_skill_kernel.conformance"]
134
+ forbidden_modules = ["plm_skill_kernel.sdk"]
135
+ ignore_imports = []