akms-learn 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. akms_learn/__init__.py +155 -0
  2. akms_learn/_code_links.py +154 -0
  3. akms_learn/adapters/__init__.py +38 -0
  4. akms_learn/adapters/fake_adapter.py +172 -0
  5. akms_learn/adapters/protocols.py +197 -0
  6. akms_learn/adapters/status.py +130 -0
  7. akms_learn/capabilities_catalog.py +263 -0
  8. akms_learn/capability_gates.py +293 -0
  9. akms_learn/cli.py +563 -0
  10. akms_learn/compiler.py +1127 -0
  11. akms_learn/domain_packs/__init__.py +51 -0
  12. akms_learn/domain_packs/capabilities.py +49 -0
  13. akms_learn/domain_packs/descriptors.py +208 -0
  14. akms_learn/domain_packs/registry.py +133 -0
  15. akms_learn/domain_packs/warnings.py +65 -0
  16. akms_learn/exporters/__init__.py +89 -0
  17. akms_learn/exporters/_mathaware.py +119 -0
  18. akms_learn/exporters/assessment.py +369 -0
  19. akms_learn/exporters/bundle.py +332 -0
  20. akms_learn/exporters/html.py +354 -0
  21. akms_learn/exporters/markdown.py +532 -0
  22. akms_learn/exporters/notebook.py +443 -0
  23. akms_learn/graph_import.py +363 -0
  24. akms_learn/llm/__init__.py +21 -0
  25. akms_learn/llm/no_provider_stub.py +121 -0
  26. akms_learn/llm/protocol.py +105 -0
  27. akms_learn/llm/providers/__init__.py +6 -0
  28. akms_learn/llm/providers/akms_completion.py +100 -0
  29. akms_learn/llm/providers/nlm_cli.py +187 -0
  30. akms_learn/llm/registry.py +232 -0
  31. akms_learn/models/__init__.py +76 -0
  32. akms_learn/models/assessment.py +119 -0
  33. akms_learn/models/learner_profile.py +56 -0
  34. akms_learn/models/llm_expansion.py +204 -0
  35. akms_learn/models/lsp.py +318 -0
  36. akms_learn/modes/__init__.py +3 -0
  37. akms_learn/modes/adaptive_path.py +415 -0
  38. akms_learn/modes/anthology.py +227 -0
  39. akms_learn/modes/assessment_first.py +553 -0
  40. akms_learn/modes/bundle_source.py +50 -0
  41. akms_learn/modes/derivation_first.py +469 -0
  42. akms_learn/modes/implementation_first.py +468 -0
  43. akms_learn/modes/llm_expanded.py +590 -0
  44. akms_learn/modes/multi_granularity.py +507 -0
  45. akms_learn/modes/notebook_source.py +620 -0
  46. akms_learn/modes/outline.py +232 -0
  47. akms_learn/modes/pedagogical_template.py +384 -0
  48. akms_learn/modes/pitfall.py +266 -0
  49. akms_learn/optional_metadata.py +185 -0
  50. akms_learn/ordering.py +512 -0
  51. akms_learn/plugin.py +88 -0
  52. akms_learn/profiles.py +3 -0
  53. akms_learn/requests.py +325 -0
  54. akms_learn/section_extraction.py +355 -0
  55. akms_learn/sections.py +329 -0
  56. akms_learn/toy_fixtures.py +671 -0
  57. akms_learn/validation.py +107 -0
  58. akms_learn/warnings.py +145 -0
  59. akms_learn-0.3.0.dist-info/METADATA +51 -0
  60. akms_learn-0.3.0.dist-info/RECORD +64 -0
  61. akms_learn-0.3.0.dist-info/WHEEL +5 -0
  62. akms_learn-0.3.0.dist-info/entry_points.txt +5 -0
  63. akms_learn-0.3.0.dist-info/licenses/LICENSE +201 -0
  64. akms_learn-0.3.0.dist-info/top_level.txt +1 -0
akms_learn/__init__.py ADDED
@@ -0,0 +1,155 @@
1
+ """akms_learn: Learning Source Packet compiler and exporters for AKMS."""
2
+
3
+ from akms_learn.cli import main
4
+ from akms_learn.compiler import (
5
+ STAGES,
6
+ CompileResult,
7
+ compile_learning_source,
8
+ )
9
+ from akms_learn.domain_packs import (
10
+ CapabilityStatus,
11
+ CompanionRole,
12
+ DomainPackDescriptor,
13
+ DomainPackRegistry,
14
+ DomainPackWarning,
15
+ LearningCapabilityError,
16
+ RuntimeHint,
17
+ SourcePackDescriptor,
18
+ build_registry_from_paths,
19
+ load_descriptor_from_yaml,
20
+ load_source_pack_from_yaml,
21
+ warn_planned_companion,
22
+ )
23
+ from akms_learn.exporters.bundle import MANIFEST_VERSION
24
+ from akms_learn.exporters.bundle import export as bundle_export
25
+ from akms_learn.exporters.markdown import export as markdown_export
26
+ from akms_learn.graph_import import (
27
+ GraphSlice,
28
+ compute_graph_hash,
29
+ fixture_graph,
30
+ load_graph,
31
+ )
32
+ from akms_learn.modes.anthology import (
33
+ TEACHING_SECTIONS,
34
+ AnthologyEntry,
35
+ anthology_mode,
36
+ )
37
+ from akms_learn.modes.bundle_source import bundle_source_mode
38
+ from akms_learn.modes.outline import outline_mode
39
+ from akms_learn.modes.pitfall import (
40
+ PITFALL_EDGE_TYPES,
41
+ STRUCTURED_FIELDS,
42
+ pitfall_mode,
43
+ )
44
+ from akms_learn.models import (
45
+ AssessmentView,
46
+ CodeLinkView,
47
+ CompilerInfo,
48
+ LearningEdgeView,
49
+ LearningNodeView,
50
+ LearningRequestInfo,
51
+ LearningSourcePacket,
52
+ LearningWarning,
53
+ PacketBody,
54
+ PitfallView,
55
+ ReferenceView,
56
+ SourceInfo,
57
+ )
58
+ from akms_learn.ordering import LEARNING_BUCKETS, order_nodes
59
+ from akms_learn.requests import (
60
+ LearningRequest,
61
+ normalize_request,
62
+ request_hash,
63
+ to_canonical_dict,
64
+ )
65
+ from akms_learn.section_extraction import (
66
+ APPROVED_HEADINGS,
67
+ EXCERPT_MAX_CHARS,
68
+ ExtractedSection,
69
+ ExtractionMethod,
70
+ extract_sections_from_node,
71
+ extract_sections_from_nodes,
72
+ )
73
+ from akms_learn.sections import APPROVED_SECTIONS, SectionView, extract_sections
74
+ from akms_learn.validation import PacketValidationError, validate_packet
75
+ from akms_learn.warnings import (
76
+ WarningAccumulator,
77
+ emit_dangling_reference_warning,
78
+ emit_missing_section_warning,
79
+ )
80
+
81
+ __all__ = [
82
+ # Compiler
83
+ "STAGES",
84
+ "CompileResult",
85
+ "compile_learning_source",
86
+ # Graph import
87
+ "GraphSlice",
88
+ "load_graph",
89
+ "compute_graph_hash",
90
+ "fixture_graph",
91
+ # Ordering
92
+ "LEARNING_BUCKETS",
93
+ "order_nodes",
94
+ # Sections
95
+ "APPROVED_SECTIONS",
96
+ "SectionView",
97
+ "extract_sections",
98
+ # Node-level section extraction
99
+ "APPROVED_HEADINGS",
100
+ "EXCERPT_MAX_CHARS",
101
+ "ExtractedSection",
102
+ "ExtractionMethod",
103
+ "extract_sections_from_node",
104
+ "extract_sections_from_nodes",
105
+ # LSP models
106
+ "LearningSourcePacket",
107
+ "CompilerInfo",
108
+ "SourceInfo",
109
+ "LearningRequestInfo",
110
+ "PacketBody",
111
+ "LearningNodeView",
112
+ "LearningEdgeView",
113
+ "PitfallView",
114
+ "CodeLinkView",
115
+ "AssessmentView",
116
+ "ReferenceView",
117
+ "LearningWarning",
118
+ "LearningRequest",
119
+ "normalize_request",
120
+ "request_hash",
121
+ "to_canonical_dict",
122
+ "WarningAccumulator",
123
+ "emit_missing_section_warning",
124
+ "emit_dangling_reference_warning",
125
+ "PacketValidationError",
126
+ "validate_packet",
127
+ # Domain-pack foundation
128
+ "CapabilityStatus",
129
+ "CompanionRole",
130
+ "DomainPackDescriptor",
131
+ "DomainPackRegistry",
132
+ "DomainPackWarning",
133
+ "LearningCapabilityError",
134
+ "RuntimeHint",
135
+ "SourcePackDescriptor",
136
+ "build_registry_from_paths",
137
+ "load_descriptor_from_yaml",
138
+ "load_source_pack_from_yaml",
139
+ "warn_planned_companion",
140
+ # Exporters
141
+ "markdown_export",
142
+ "bundle_export",
143
+ "MANIFEST_VERSION",
144
+ # Structured modes
145
+ "outline_mode",
146
+ "anthology_mode",
147
+ "AnthologyEntry",
148
+ "TEACHING_SECTIONS",
149
+ "pitfall_mode",
150
+ "PITFALL_EDGE_TYPES",
151
+ "STRUCTURED_FIELDS",
152
+ "bundle_source_mode",
153
+ # CLI
154
+ "main",
155
+ ]
@@ -0,0 +1,154 @@
1
+ """Shared helpers for code-link extraction.
2
+
3
+ Promoted from ``compiler.py`` and ``modes/implementation_first.py`` to
4
+ remove duplication. Lives at the package
5
+ top level so both the compiler stage and the ``implementation_first`` mode
6
+ can call into the same authoritative implementation.
7
+
8
+ Surface
9
+ -------
10
+ * :data:`MISSING_SOURCE_PATH_SENTINELS` — frozenset of placeholder strings
11
+ that count as "no usable source path" (``"", "unknown", "none", "null"``).
12
+ * :func:`is_missing_source_path` — predicate over a raw ``source_path``
13
+ value; case-insensitive, whitespace-tolerant, None-safe.
14
+ * :func:`coerce_line_range` — best-effort coercion of a node/edge
15
+ ``line_range`` value into a ``(start, end)`` integer tuple.
16
+ * :func:`build_code_links` — walks ``implements`` edges and emits one
17
+ :class:`CodeLinkView` per edge. Optionally invokes a missing-source
18
+ callback so the compiler can emit ``code_mirror_missing_source_path``
19
+ warnings while the mode keeps its own simpler warning path.
20
+
21
+ Callers
22
+ -------
23
+ * ``compiler._build_code_links`` thin-wraps :func:`build_code_links`
24
+ passing the WarningAccumulator-driven callback.
25
+ * ``implementation_first._build_code_references`` uses the no-callback
26
+ form; it emits ``implementation_anchor_missing_source`` separately
27
+ via :func:`implementation_first._emit_anchor_missing_source_warnings`.
28
+
29
+ The module has no LLM imports.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Any, Callable, Optional
35
+
36
+ from akms_learn.models import CodeLinkView
37
+
38
+ __all__ = [
39
+ "MISSING_SOURCE_PATH_SENTINELS",
40
+ "is_missing_source_path",
41
+ "coerce_line_range",
42
+ "build_code_links",
43
+ ]
44
+
45
+
46
+ MISSING_SOURCE_PATH_SENTINELS: frozenset[str] = frozenset(
47
+ {"", "unknown", "none", "null"}
48
+ )
49
+
50
+
51
+ def is_missing_source_path(value: Any) -> bool:
52
+ """Return True if *value* is an unusable placeholder source path.
53
+
54
+ Treats ``None``, empty string, and the canonical sentinels
55
+ (``"unknown"`` / ``"none"`` / ``"null"``) as missing. Comparison is
56
+ case-insensitive after stripping whitespace.
57
+ """
58
+ if value is None:
59
+ return True
60
+ text = str(value).strip().lower()
61
+ return text in MISSING_SOURCE_PATH_SENTINELS
62
+
63
+
64
+ def coerce_line_range(value: Any) -> tuple[int, int]:
65
+ """Best-effort coercion of a node/edge ``line_range`` to ``(start, end)``.
66
+
67
+ Returns ``(0, 0)`` for any malformed input (non-list, wrong length,
68
+ non-integer elements). The placeholder pair is recognised by callers
69
+ that want to skip uninformative line ranges.
70
+ """
71
+ if isinstance(value, (list, tuple)) and len(value) == 2:
72
+ try:
73
+ return (int(value[0]), int(value[1]))
74
+ except (TypeError, ValueError):
75
+ pass
76
+ return (0, 0)
77
+
78
+
79
+ def build_code_links(
80
+ edges: tuple[dict[str, Any], ...] | list[dict[str, Any]],
81
+ nodes_by_id: dict[str, dict[str, Any]],
82
+ on_missing_mirror_source: Optional[Callable[[str, str], None]] = None,
83
+ ) -> list[CodeLinkView]:
84
+ """Walk ``implements`` edges and emit one :class:`CodeLinkView` per edge.
85
+
86
+ Behaviour:
87
+
88
+ * One :class:`CodeLinkView` per edge with ``type == "implements"``.
89
+ * ``source_node_id`` is the edge's ``from`` endpoint.
90
+ * ``target`` resolves to the target node id (always set when present).
91
+ * ``relation`` is fixed to ``"implements"``.
92
+ * ``file_path`` and ``line_range`` come from the target node when the
93
+ target has a usable ``source_path``; otherwise the fields stay
94
+ ``None``.
95
+ * When *on_missing_mirror_source* is supplied AND the target node is a
96
+ ``code_mirror`` with a missing/sentinel ``source_path``, the callback
97
+ is invoked once per such edge with ``(mirror_node_id, edge_id)``.
98
+ The mode-level caller (``implementation_first``) passes ``None`` so
99
+ it can emit its own ``implementation_anchor_missing_source`` warning
100
+ via a separate code path.
101
+
102
+ The collector is order-stable: edges are processed in the order they
103
+ arrive in (assumed already sorted by the caller) and the returned list
104
+ preserves that order.
105
+ """
106
+ code_links: list[CodeLinkView] = []
107
+ for edge in edges:
108
+ if edge.get("type") != "implements":
109
+ continue
110
+
111
+ source_id = str(edge.get("from") or "")
112
+ target_id = str(edge.get("to") or "")
113
+ edge_id = str(edge.get("edge_id") or "")
114
+
115
+ target_node = nodes_by_id.get(target_id) or {}
116
+ target_source_path = target_node.get("source_path")
117
+ line_range_raw = target_node.get("line_range")
118
+
119
+ usable_path: Optional[str] = (
120
+ None
121
+ if is_missing_source_path(target_source_path)
122
+ else str(target_source_path)
123
+ )
124
+ usable_line_range: Optional[tuple[int, int]] = None
125
+ if usable_path is not None and line_range_raw is not None:
126
+ coerced = coerce_line_range(line_range_raw)
127
+ if coerced != (0, 0):
128
+ usable_line_range = coerced
129
+
130
+ target_value = target_id or (usable_path or "")
131
+
132
+ code_links.append(
133
+ CodeLinkView(
134
+ node_id=source_id or target_id or edge_id,
135
+ source_file=usable_path or "unknown",
136
+ source_node_id=source_id or None,
137
+ target=target_value or None,
138
+ relation="implements",
139
+ file_path=usable_path,
140
+ line_range=usable_line_range,
141
+ mirror_node_id=(
142
+ target_id if target_node.get("kind") == "code_mirror" else None
143
+ ),
144
+ )
145
+ )
146
+
147
+ if (
148
+ on_missing_mirror_source is not None
149
+ and target_node.get("kind") == "code_mirror"
150
+ and is_missing_source_path(target_source_path)
151
+ ):
152
+ on_missing_mirror_source(target_id, edge_id)
153
+
154
+ return code_links
@@ -0,0 +1,38 @@
1
+ """Adapter protocols package for akms-learn.
2
+
3
+ This package defines pure-Python protocol surfaces for four advanced-companion
4
+ adapters. No real adapter implementations ship here; all concrete
5
+ implementations are provided by downstream packages.
6
+
7
+ No-mutation invariant
8
+ ---------------------
9
+ Adapters in this package MUST NOT:
10
+ - Write to the AKMS global vault or any AKMS-owned path.
11
+ - Mutate any AKMS graph object passed to them.
12
+ - Write files via any file-write API targeting AKMS-owned paths.
13
+
14
+ Public surface
15
+ --------------
16
+ ``protocols`` — the four runtime_checkable Protocol classes.
17
+ ``status`` — AdapterStatus enum + adapter_registry() accessor.
18
+ ``fake_adapter``— Fake implementations for use in tests ONLY.
19
+ """
20
+
21
+ from akms_learn.adapters.protocols import (
22
+ ConceptKitAdapter,
23
+ ExecutableBridgeAdapter,
24
+ NotebookExecutionAdapter,
25
+ PedagogicalWorkbenchAdapter,
26
+ )
27
+ from akms_learn.adapters.status import AdapterStatus, adapter_registry
28
+
29
+ __all__ = [
30
+ # Protocols
31
+ "ConceptKitAdapter",
32
+ "ExecutableBridgeAdapter",
33
+ "NotebookExecutionAdapter",
34
+ "PedagogicalWorkbenchAdapter",
35
+ # Status
36
+ "AdapterStatus",
37
+ "adapter_registry",
38
+ ]
@@ -0,0 +1,172 @@
1
+ """Fake adapter implementations for tests ONLY.
2
+
3
+ WARNING: These classes are for use in tests ONLY. They are intentionally
4
+ trivial; they return hard-coded stub payloads and do NOT perform any real
5
+ computation. Never import fake_adapter in production code.
6
+
7
+ No-mutation invariant
8
+ ---------------------
9
+ Fake adapters MUST NOT write to any AKMS path. All output is returned in
10
+ memory as plain Python dicts.
11
+
12
+ Protocol conformance
13
+ --------------------
14
+ Each class satisfies its corresponding Protocol via structural subtyping.
15
+ Because the Protocols are ``@runtime_checkable``, the isinstance checks::
16
+
17
+ isinstance(FakeConceptKit(), ConceptKitAdapter)
18
+ isinstance(FakePedagogicalWorkbench(), PedagogicalWorkbenchAdapter)
19
+ isinstance(FakeExecutableBridge(), ExecutableBridgeAdapter)
20
+ isinstance(FakeNotebookExecution(), NotebookExecutionAdapter)
21
+
22
+ all return True.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+ from akms_learn.adapters.protocols import (
30
+ ConceptKitAdapter,
31
+ ExecutableBridgeAdapter,
32
+ NotebookExecutionAdapter,
33
+ PedagogicalWorkbenchAdapter,
34
+ )
35
+
36
+ __all__ = [
37
+ "FakeConceptKit",
38
+ "FakeExecutableBridge",
39
+ "FakeNotebookExecution",
40
+ "FakePedagogicalWorkbench",
41
+ ]
42
+
43
+
44
+ class FakeConceptKit:
45
+ """Fake ConceptKitAdapter — returns a deterministic stub payload.
46
+
47
+ Satisfies :class:`~akms_learn.adapters.protocols.ConceptKitAdapter`
48
+ structurally (and via ``isinstance`` due to ``@runtime_checkable``).
49
+ """
50
+
51
+ def generate_concept_kit(
52
+ self,
53
+ excerpt: dict[str, Any],
54
+ *,
55
+ options: dict[str, Any] | None = None,
56
+ ) -> dict[str, Any]:
57
+ """Return a deterministic stub concept-kit payload."""
58
+ return {
59
+ "adapter": "FakeConceptKit",
60
+ "status": "ok",
61
+ "concepts": ["stub_concept_A", "stub_concept_B"],
62
+ "excerpt_keys": sorted(excerpt.keys()),
63
+ "options_received": options is not None,
64
+ }
65
+
66
+
67
+ class FakePedagogicalWorkbench:
68
+ """Fake PedagogicalWorkbenchAdapter — returns a deterministic stub payload.
69
+
70
+ Satisfies :class:`~akms_learn.adapters.protocols.PedagogicalWorkbenchAdapter`
71
+ structurally (and via ``isinstance`` due to ``@runtime_checkable``).
72
+ """
73
+
74
+ def analyse_pedagogy(
75
+ self,
76
+ excerpt: dict[str, Any],
77
+ *,
78
+ options: dict[str, Any] | None = None,
79
+ ) -> dict[str, Any]:
80
+ """Return a deterministic stub pedagogical analysis payload."""
81
+ return {
82
+ "adapter": "FakePedagogicalWorkbench",
83
+ "status": "ok",
84
+ "objectives": ["stub_objective_1"],
85
+ "difficulty": "intermediate",
86
+ "excerpt_keys": sorted(excerpt.keys()),
87
+ "options_received": options is not None,
88
+ }
89
+
90
+
91
+ class FakeExecutableBridge:
92
+ """Fake ExecutableBridgeAdapter — returns a deterministic stub payload.
93
+
94
+ Satisfies :class:`~akms_learn.adapters.protocols.ExecutableBridgeAdapter`
95
+ structurally (and via ``isinstance`` due to ``@runtime_checkable``).
96
+ """
97
+
98
+ def build_executable(
99
+ self,
100
+ excerpt: dict[str, Any],
101
+ *,
102
+ options: dict[str, Any] | None = None,
103
+ ) -> dict[str, Any]:
104
+ """Return a deterministic stub executable-artefact payload."""
105
+ return {
106
+ "adapter": "FakeExecutableBridge",
107
+ "status": "ok",
108
+ "executable_type": "stub",
109
+ "source": "# stub source",
110
+ "excerpt_keys": sorted(excerpt.keys()),
111
+ "options_received": options is not None,
112
+ }
113
+
114
+
115
+ class FakeNotebookExecution:
116
+ """Fake NotebookExecutionAdapter — returns a deterministic stub payload.
117
+
118
+ Satisfies :class:`~akms_learn.adapters.protocols.NotebookExecutionAdapter`
119
+ structurally (and via ``isinstance`` due to ``@runtime_checkable``).
120
+
121
+ The stub cells include all three notebook-metadata keys described in the
122
+ ``NotebookExecutionAdapter`` docstring (``no_execute``,
123
+ ``illustrative_only``, ``adapter_executable``) so that downstream
124
+ notebook-exporter tests can rely on them.
125
+ """
126
+
127
+ def build_notebook(
128
+ self,
129
+ excerpt: dict[str, Any],
130
+ *,
131
+ options: dict[str, Any] | None = None,
132
+ ) -> dict[str, Any]:
133
+ """Return a deterministic stub notebook payload."""
134
+ stub_cell = {
135
+ "cell_type": "code",
136
+ "source": "# stub cell",
137
+ "metadata": {
138
+ "no_execute": False,
139
+ "illustrative_only": True,
140
+ "adapter_executable": False,
141
+ },
142
+ "outputs": [],
143
+ }
144
+ return {
145
+ "adapter": "FakeNotebookExecution",
146
+ "status": "ok",
147
+ "nbformat": 4,
148
+ "nbformat_minor": 5,
149
+ "cells": [stub_cell],
150
+ "excerpt_keys": sorted(excerpt.keys()),
151
+ "options_received": options is not None,
152
+ }
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Runtime isinstance-compatibility assertions (module-level, evaluated once).
157
+ # ---------------------------------------------------------------------------
158
+ # These raise AssertionError at import time if the structural subtyping is
159
+ # accidentally broken, making test failures loud and immediate.
160
+
161
+ assert isinstance(FakeConceptKit(), ConceptKitAdapter), (
162
+ "FakeConceptKit must satisfy ConceptKitAdapter protocol"
163
+ )
164
+ assert isinstance(FakePedagogicalWorkbench(), PedagogicalWorkbenchAdapter), (
165
+ "FakePedagogicalWorkbench must satisfy PedagogicalWorkbenchAdapter protocol"
166
+ )
167
+ assert isinstance(FakeExecutableBridge(), ExecutableBridgeAdapter), (
168
+ "FakeExecutableBridge must satisfy ExecutableBridgeAdapter protocol"
169
+ )
170
+ assert isinstance(FakeNotebookExecution(), NotebookExecutionAdapter), (
171
+ "FakeNotebookExecution must satisfy NotebookExecutionAdapter protocol"
172
+ )