plm-skill-kernel 1.2.0__tar.gz → 1.3.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 (81) hide show
  1. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/PKG-INFO +2 -1
  2. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/__init__.py +1 -1
  3. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/mocks/__init__.py +22 -1
  4. plm_skill_kernel-1.3.0/plm_skill_kernel/mocks/connectors.py +182 -0
  5. plm_skill_kernel-1.3.0/plm_skill_kernel/mocks/engine_core_stub.py +136 -0
  6. plm_skill_kernel-1.3.0/plm_skill_kernel/mocks/knowledge_services_port.py +359 -0
  7. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/PKG-INFO +2 -1
  8. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/SOURCES.txt +6 -0
  9. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/requires.txt +1 -0
  10. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/pyproject.toml +10 -1
  11. plm_skill_kernel-1.3.0/tests/test_connector_read_mock.py +128 -0
  12. plm_skill_kernel-1.3.0/tests/test_engine_core_stub_mock.py +141 -0
  13. plm_skill_kernel-1.3.0/tests/test_knowledge_services_port_mock.py +176 -0
  14. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/README.md +0 -0
  15. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/__main__.py +0 -0
  16. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/__init__.py +0 -0
  17. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_cli.py +0 -0
  18. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_output_safety.py +0 -0
  19. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_serialize.py +0 -0
  20. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_terminal.py +0 -0
  21. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_verdict.py +0 -0
  22. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_version.py +0 -0
  23. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/exit_codes.py +0 -0
  24. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/models.py +0 -0
  25. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/protocol.py +0 -0
  26. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/dispatcher.py +0 -0
  27. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/__init__.py +0 -0
  28. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/_catalog_fixture.py +0 -0
  29. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/binding.py +0 -0
  30. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/executor.py +0 -0
  31. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/knowledge.py +0 -0
  32. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/plan.py +0 -0
  33. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/request.py +0 -0
  34. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/selector.py +0 -0
  35. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/mocks/foundation_seed.py +0 -0
  36. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/registry.py +0 -0
  37. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/__init__.py +0 -0
  38. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/_registry.py +0 -0
  39. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/context.py +0 -0
  40. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/decorator.py +0 -0
  41. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/types.py +0 -0
  42. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/server.py +0 -0
  43. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/__init__.py +0 -0
  44. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/agents/__init__.py +0 -0
  45. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/agents/suggest.py +0 -0
  46. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/bpmn/__init__.py +0 -0
  47. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/bpmn/generate.py +0 -0
  48. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/cleansing/__init__.py +0 -0
  49. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/cleansing/dedupe.py +0 -0
  50. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/cleansing/normalise.py +0 -0
  51. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/documents/__init__.py +0 -0
  52. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/documents/parse.py +0 -0
  53. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/__init__.py +0 -0
  54. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/_v1_corpus.py +0 -0
  55. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/get_record.py +0 -0
  56. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/resolve.py +0 -0
  57. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/retrieve.py +0 -0
  58. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/search.py +0 -0
  59. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/validate.py +0 -0
  60. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/__init__.py +0 -0
  61. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/knowledge_pack_loader.py +0 -0
  62. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/knowledge_search_engine.py +0 -0
  63. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/plm_skill_registry.py +0 -0
  64. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/plm_tool_definitions.py +0 -0
  65. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/skill_orchestrator.py +0 -0
  66. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/dependency_links.txt +0 -0
  67. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/entry_points.txt +0 -0
  68. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/top_level.txt +0 -0
  69. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/setup.cfg +0 -0
  70. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_agents_suggest_asgi.py +0 -0
  71. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_agents_suggest_unit.py +0 -0
  72. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_dispatcher.py +0 -0
  73. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_documents_parse_asgi.py +0 -0
  74. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_documents_parse_unit.py +0 -0
  75. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_executor.py +0 -0
  76. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_foundation_mocks.py +0 -0
  77. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_sdk_decorator.py +0 -0
  78. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_server.py +0 -0
  79. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_skills_cleansing.py +0 -0
  80. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_skills_knowledge.py +0 -0
  81. {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.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.2.0
3
+ Version: 1.3.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
@@ -13,6 +13,7 @@ Requires-Dist: uvicorn>=0.27
13
13
  Requires-Dist: httpx>=0.27.0
14
14
  Requires-Dist: plm-engine-core
15
15
  Requires-Dist: plm-knowledge
16
+ Requires-Dist: plm-connector-sdk<2,>=1.1
16
17
  Provides-Extra: test
17
18
  Requires-Dist: pytest; extra == "test"
18
19
  Requires-Dist: pytest-asyncio; extra == "test"
@@ -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.2.0"
17
+ __version__ = "1.3.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
@@ -50,4 +50,25 @@ def require_mocks(what: str = "this mock") -> None:
50
50
  )
51
51
 
52
52
 
53
- __all__ = ["MOCKS_ENV", "MocksDisabled", "mocks_enabled", "require_mocks"]
53
+ # ── governed contract package versions this mocks layer targets (AC4) ──────
54
+ # The MVP mocks stand in for three governed, separately-versioned packages. This
55
+ # dict is the single authoritative record of the exact versions the mocks were
56
+ # written against; ``tests/contracts/test_mvp_mocks_boundary.py`` asserts each
57
+ # against what is actually installed. A governed-package bump therefore surfaces
58
+ # as a failing pin that a human re-syncs on purpose (updating MOCKS.md), never a
59
+ # silent absorption. Per-mock *shape* pins (KS_CONTRACT_VERSION,
60
+ # CONNECTOR_SDK_VERSION, SELECTION_REQUIRED_KEYS) live in their own modules.
61
+ GOVERNED_CONTRACT_VERSIONS = {
62
+ "plm-shared": "1.2.1", # CR.10/CR.12 wire shapes the SDK + dispatcher consume
63
+ "plm-connector-sdk": "1.3.1", # ConnectorReadResult — AC2 reaches a connector through it
64
+ "plm-engine-core": "1.6.0", # runtime.llm_service — the AC3 stub stands in for it
65
+ }
66
+
67
+
68
+ __all__ = [
69
+ "GOVERNED_CONTRACT_VERSIONS",
70
+ "MOCKS_ENV",
71
+ "MocksDisabled",
72
+ "mocks_enabled",
73
+ "require_mocks",
74
+ ]
@@ -0,0 +1,182 @@
1
+ """3DEXPERIENCE connector reached behind its real ``ConnectorReadResult`` (FTR-2234 / E3, AC2).
2
+
3
+ # MOCK(MVP-DEMO): revert / re-sync under FTR-2234.
4
+ #
5
+ # AC2 is explicit: *reuse the existing connector mock, build no new one.* So this
6
+ # module holds **no fixture data and no fake connector**. It wraps the portfolio's
7
+ # already-shipped, fixture-backed ``Dassault3dexperienceMockConnector``
8
+ # (``plm_mcp_connectors``) and exposes its reads through the connector SDK's real,
9
+ # frozen ``ConnectorReadResult`` contract (``plm_connector_sdk`` v1.3.1). The kernel
10
+ # reaches a connector *only* over this seam.
11
+ #
12
+ # Layering (mirrors reality, so the swap is a drop-in):
13
+ #
14
+ # kernel (E4/E5, later)
15
+ # └── ConnectorReadPort.read(method, params) ← reversible seam (this module)
16
+ # └── ConnectorReadResult ← connector SDK contract (real)
17
+ # └── Dassault3dexperienceMockConnector ← existing fixture mock
18
+ # └── in-tree fixture rows (ENG-0001..0003)
19
+ #
20
+ # When a real connector is wired, the reader is handed engine-core's live
21
+ # ``ConnectorRuntime`` instead of the fixture connector; it still returns the same
22
+ # ``ConnectorReadResult``, so the kernel and every task are untouched.
23
+ #
24
+ # Honesty, two ways:
25
+ # * the seam stays **async**. Unlike the E1 knowledge seam (pure in-memory, so
26
+ # AC1 could drive it synchronously), a real connector performs network I/O and
27
+ # must be reached through an async call path — so this reader never pretends a
28
+ # connector read is synchronous, even though the *fixture* behind it is.
29
+ # * the wrapped connector's identity is literally ``dassault_3dexperience_mock``;
30
+ # ``PRODUCTION_BACKED`` is ``False`` and stays that way.
31
+ #
32
+ # Everything here is quarantined behind ``PLM_MVP_MOCKS`` (see
33
+ # :func:`plm_skill_kernel.mocks.require_mocks`). The fixture host
34
+ # (``plm_mcp_connectors``) is imported **lazily**, inside the constructor, so this
35
+ # module stays importable — for the frozen-contract pin below — in an environment
36
+ # that has the connector SDK but not the demo fixture package.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ from typing import Any, List, Mapping, Optional, Protocol, runtime_checkable
41
+
42
+ # The connector SDK is a governed contract package (like plm-shared), pinned as a
43
+ # kernel dependency, so its frozen result type is imported at module top. The
44
+ # *fixture host* is not a kernel dependency and is imported lazily below.
45
+ from plm_connector_sdk.context import ConnectorContext
46
+ from plm_connector_sdk.types import ConnectorReadRequest, ConnectorReadResult
47
+
48
+ from . import require_mocks
49
+
50
+ # ── pinned connector SDK identity ──────────────────────────────────────────
51
+ # Mirror of plm-connector-sdk's package version. Pinned as a literal (the SDK
52
+ # does not export ``__version__``) so the freeze test asserts the exact contract
53
+ # generation this mock was written against; a bump must be re-synced on purpose.
54
+ CONNECTOR_SDK_VERSION = "1.3.1"
55
+
56
+ # The fixture connector this mock reuses (built under plm-mcp-connectors, NOT here).
57
+ FIXTURE_CONNECTOR_MODULE = "plm_mcp_connectors.dassault_3dexperience_mock"
58
+
59
+ # The read methods the fixture connector serves (§ FTR-2234 AC2). Asking for
60
+ # anything else is a programming error, not a connector "not found".
61
+ SUPPORTED_METHODS = frozenset({"get_eng_item", "search_eng_items", "get_bom"})
62
+
63
+ DEFAULT_TENANT = "mvp-demo"
64
+
65
+
66
+ class UnsupportedConnectorMethod(RuntimeError):
67
+ """Raised when the reader is asked for a method the fixture connector lacks."""
68
+
69
+
70
+ @runtime_checkable
71
+ class ConnectorReadPort(Protocol):
72
+ """The reversible connector seam: any object that answers a named read with a
73
+ :class:`~plm_connector_sdk.types.ConnectorReadResult`.
74
+
75
+ The demo satisfies it with :class:`FixtureConnectorReader` (wrapping the
76
+ fixture mock); a real deployment satisfies it with an engine-core connector
77
+ runtime. The kernel depends only on this surface, never on which side backs it.
78
+ """
79
+
80
+ async def read(
81
+ self, method: str, params: Mapping[str, Any], *, tenant_id: str = ...
82
+ ) -> ConnectorReadResult:
83
+ ...
84
+
85
+
86
+ class FixtureConnectorReader:
87
+ """Reads the existing 3DEXPERIENCE **fixture** connector behind ``ConnectorReadResult`` (AC2).
88
+
89
+ Builds no connector and no fixture data of its own: it constructs the
90
+ portfolio's :class:`Dassault3dexperienceMockConnector` and forwards each read
91
+ to it, wrapping the call in the SDK's real ``ConnectorReadRequest`` /
92
+ ``ConnectorContext`` and returning the connector's real ``ConnectorReadResult``.
93
+
94
+ Gated: constructing without ``PLM_MVP_MOCKS`` raises
95
+ :class:`~plm_skill_kernel.mocks.MocksDisabled`.
96
+ """
97
+
98
+ # Honesty flags — parity with the KS mock's descriptor (AC1). A fixture
99
+ # connector is never a production claim.
100
+ PRODUCTION_BACKED = False
101
+ TEST_ONLY = True
102
+
103
+ def __init__(self, *, tenant_id: str = DEFAULT_TENANT) -> None:
104
+ require_mocks("FixtureConnectorReader")
105
+ # Lazy import: keeps this module importable (for the frozen-contract pin)
106
+ # without the demo fixture package present.
107
+ import importlib
108
+
109
+ fixture = importlib.import_module(FIXTURE_CONNECTOR_MODULE)
110
+ self._connector = fixture.Dassault3dexperienceMockConnector()
111
+ self.connector_id: str = fixture.CONNECTOR_ID
112
+ self.connector_version: str = fixture.CONNECTOR_VERSION
113
+ self._tenant_id = tenant_id
114
+
115
+ async def read(
116
+ self,
117
+ method: str,
118
+ params: Mapping[str, Any],
119
+ *,
120
+ tenant_id: Optional[str] = None,
121
+ ) -> ConnectorReadResult:
122
+ """Forward a named read to the fixture connector, returning its ``ConnectorReadResult``.
123
+
124
+ ``method`` must be one of :data:`SUPPORTED_METHODS`; an unknown method is a
125
+ caller bug (:class:`UnsupportedConnectorMethod`), distinct from a connector
126
+ "not found" — which the connector itself reports as ``ok=False`` with an
127
+ error envelope.
128
+ """
129
+ if method not in SUPPORTED_METHODS:
130
+ raise UnsupportedConnectorMethod(
131
+ f"{method!r} is not served by {self.connector_id}; "
132
+ f"supported: {sorted(SUPPORTED_METHODS)}"
133
+ )
134
+ request = ConnectorReadRequest(
135
+ params=dict(params), version=self.connector_version
136
+ )
137
+ context = ConnectorContext(
138
+ connector_id=self.connector_id,
139
+ connector_version=self.connector_version,
140
+ method_name=method,
141
+ tenant_id=tenant_id or self._tenant_id,
142
+ )
143
+ bound = getattr(self._connector, method)
144
+ return await bound(request, context)
145
+
146
+ # ── convenience wrappers over read() (the three fixture reads) ──────────
147
+ async def get_eng_item(
148
+ self, physical_id: str, *, tenant_id: Optional[str] = None
149
+ ) -> ConnectorReadResult:
150
+ return await self.read(
151
+ "get_eng_item", {"physical_id": physical_id}, tenant_id=tenant_id
152
+ )
153
+
154
+ async def search_eng_items(
155
+ self,
156
+ query: str,
157
+ *,
158
+ max_results: Optional[int] = None,
159
+ tenant_id: Optional[str] = None,
160
+ ) -> ConnectorReadResult:
161
+ params: dict[str, Any] = {"query": query}
162
+ if max_results is not None:
163
+ params["max_results"] = max_results
164
+ return await self.read("search_eng_items", params, tenant_id=tenant_id)
165
+
166
+ async def get_bom(
167
+ self, physical_id: str, *, tenant_id: Optional[str] = None
168
+ ) -> ConnectorReadResult:
169
+ return await self.read(
170
+ "get_bom", {"physical_id": physical_id}, tenant_id=tenant_id
171
+ )
172
+
173
+
174
+ __all__: List[str] = [
175
+ "CONNECTOR_SDK_VERSION",
176
+ "DEFAULT_TENANT",
177
+ "FIXTURE_CONNECTOR_MODULE",
178
+ "SUPPORTED_METHODS",
179
+ "ConnectorReadPort",
180
+ "FixtureConnectorReader",
181
+ "UnsupportedConnectorMethod",
182
+ ]
@@ -0,0 +1,136 @@
1
+ """Core LLM / DB paths mocked, kept OFF the critical path (FTR-2234 / E3, AC3).
2
+
3
+ # MOCK(MVP-DEMO): revert / re-sync under FTR-2234.
4
+ #
5
+ # The MVP critical path — parse_request -> select_skill -> SkillExecutor.run ->
6
+ # ordered tasks -> KnowledgeProvider.fetch — is deterministic and touches
7
+ # neither an LLM nor a database. Determinism is the property that makes the demo
8
+ # reproducible, not a lack of ambition: the request is routed by keyword rules
9
+ # (executor/selector.py), not by a model.
10
+ #
11
+ # There are exactly two ways the *kernel* could reach engine-core at runtime,
12
+ # and both are behind the federation boundary import-linter already fences off
13
+ # (Decision #51, CR.9 §6 — see pyproject.toml [tool.importlinter]):
14
+ #
15
+ # * the LLM path — the legacy ``SkillOrchestrator`` (plan-mode selection) does
16
+ # ``from plm_engine_core.runtime.llm_service import llm_service`` lazily,
17
+ # inside ``select_skill`` (skill_orchestrator.py:115). That single line is
18
+ # the *only* allowlisted exception to "the kernel must not import
19
+ # plm_engine_core". The MVP demo path never calls it.
20
+ # * the DB path — engine-core owns persistence. The kernel has NO database
21
+ # seam of its own: nothing under ``plm_skill_kernel`` imports a DB driver or
22
+ # ``plm_engine_core.*`` persistence. So there is nothing kernel-side to
23
+ # "mock" for the DB — the import-linter forbidden contract already keeps that
24
+ # path off every kernel module, critical or not.
25
+ #
26
+ # This module supplies the LLM half: a deterministic, in-memory stand-in for the
27
+ # ``llm_service`` singleton, so that IF the legacy plan-mode selection path is
28
+ # exercised in a demo it runs against canned, schema-valid output instead of
29
+ # LiteLLM + a live model — importing neither ``plm_engine_core`` nor ``litellm``.
30
+ #
31
+ # What it is NOT: it is never imported by any production path and never wired
32
+ # into the executor. A caller opts in explicitly, under ``PLM_MVP_MOCKS``. The
33
+ # freeze pin (tests/contracts/test_mvp_engine_core_stub_freeze.py) asserts, by
34
+ # source scan, that this file imports no engine-core / LiteLLM symbol — so the
35
+ # "no heavy import" promise cannot rot silently.
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ from typing import Any, Dict, List, Optional
41
+
42
+ from . import require_mocks
43
+
44
+ # ── the reply contract the legacy orchestrator expects ─────────────────────
45
+ # SkillOrchestrator.generate_reply returns a *string* that json.loads-es into an
46
+ # object with exactly these keys (its ``_SELECTION_SCHEMA["required"]``). Pinned
47
+ # here as a literal; the behavioural test cross-checks it against the real
48
+ # orchestrator schema so the two cannot drift apart unnoticed.
49
+ SELECTION_REQUIRED_KEYS = ("profile_id", "reasoning", "confidence", "considered_profiles")
50
+
51
+ # The orchestrator's own documented fallback profile id (used when no candidate
52
+ # keyword matches). Kept as a literal so this module needs no registry import.
53
+ DEFAULT_PROFILE_ID = "generique-plm"
54
+
55
+
56
+ class StubLLMService:
57
+ """Deterministic, in-memory stand-in for ``plm_engine_core.runtime.llm_service``.
58
+
59
+ Satisfies the single coroutine the legacy :class:`SkillOrchestrator` calls —
60
+ ``generate_reply(system_prompt, user_prompt, temperature)`` — and returns a
61
+ canned, schema-valid selection JSON *string* (never touching a model). Same
62
+ ``user_prompt`` in → same reply out; there is no clock, no network, no
63
+ provider, and no ``plm_engine_core`` / ``litellm`` import anywhere in reach.
64
+
65
+ Gated: constructing without ``PLM_MVP_MOCKS`` raises
66
+ :class:`~plm_skill_kernel.mocks.MocksDisabled`.
67
+
68
+ A caller may pass ``candidates`` (the valid profile ids for the run); the stub
69
+ picks, deterministically, the first candidate whose id appears as a substring
70
+ of the user prompt, else :attr:`DEFAULT_PROFILE_ID`. That keeps the fake
71
+ "selection" explainable without pretending to be intelligent.
72
+ """
73
+
74
+ # Honesty flags — parity with the KS mock (AC1) and connector reader (AC2).
75
+ PRODUCTION_BACKED = False
76
+ TEST_ONLY = True
77
+
78
+ def __init__(
79
+ self,
80
+ *,
81
+ profile_id: str = DEFAULT_PROFILE_ID,
82
+ confidence: float = 0.9,
83
+ candidates: Optional[List[str]] = None,
84
+ ) -> None:
85
+ require_mocks("StubLLMService")
86
+ self._profile_id = profile_id
87
+ self._confidence = float(confidence)
88
+ # Candidate ids to route among; defaults to just the fallback profile.
89
+ self._candidates: List[str] = list(candidates or [profile_id])
90
+ # Every call recorded verbatim, so a test can assert the seam was used
91
+ # and with what prompts — without a real model to introspect.
92
+ self.calls: List[Dict[str, Any]] = []
93
+
94
+ async def generate_reply(
95
+ self, *, system_prompt: str, user_prompt: str, temperature: float = 0.1
96
+ ) -> str:
97
+ """Return canned, schema-valid selection JSON (a *string*, like the real service).
98
+
99
+ Keyword-only args mirror the orchestrator's call site
100
+ (``generate_reply(system_prompt=..., user_prompt=..., temperature=0.1)``).
101
+ """
102
+ self.calls.append(
103
+ {
104
+ "system_prompt": system_prompt,
105
+ "user_prompt": user_prompt,
106
+ "temperature": temperature,
107
+ }
108
+ )
109
+ chosen = self._select(user_prompt)
110
+ payload = {
111
+ "profile_id": chosen,
112
+ "reasoning": "Deterministic MVP stub selection (no live model was called).",
113
+ "confidence": self._confidence,
114
+ # top-3 considered, selected id first — matches the schema's intent
115
+ "considered_profiles": self._considered(chosen),
116
+ }
117
+ return json.dumps(payload, ensure_ascii=False)
118
+
119
+ # ── deterministic, explainable "selection" ─────────────────────────────
120
+ def _select(self, user_prompt: str) -> str:
121
+ hay = (user_prompt or "").lower()
122
+ for candidate in self._candidates:
123
+ if candidate.lower() in hay:
124
+ return candidate
125
+ return self._profile_id
126
+
127
+ def _considered(self, chosen: str) -> List[str]:
128
+ ordered = [chosen] + [c for c in self._candidates if c != chosen]
129
+ return ordered[:3]
130
+
131
+
132
+ __all__: List[str] = [
133
+ "DEFAULT_PROFILE_ID",
134
+ "SELECTION_REQUIRED_KEYS",
135
+ "StubLLMService",
136
+ ]