plm-knowledge 1.0.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.
@@ -0,0 +1,155 @@
1
+ """
2
+ Issue Classification Dictionary and Severity Policy (US-42.3 / FTR-42).
3
+
4
+ Governance asset — pure Python data structures (NOT persisted in DB).
5
+ Maps finding_type → issue classification metadata for deterministic,
6
+ traceable severity and resolution-category assignment.
7
+ """
8
+ from dataclasses import dataclass
9
+ from typing import Dict, Optional
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class IssueClassificationEntry:
14
+ """Immutable classification record for a known issue type."""
15
+ issue_code: str # e.g. "STR_ORPHAN_NODE"
16
+ issue_label: str # e.g. "Orphan node"
17
+ issue_category: str # structural | data_quality | configuration
18
+ default_severity: str # critical | high | medium | low
19
+ default_priority: int # 0 = highest
20
+ default_resolution_category: str # auto_fix | suggested_remediation | business_decision_required
21
+ requires_business_review: bool
22
+ structural_scope_type: str # local_node | subtree | multi_node | structure_wide
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Severity Policy — explicit operational meaning for each level
27
+ # ---------------------------------------------------------------------------
28
+ SEVERITY_POLICY: Dict[str, str] = {
29
+ "critical": "Issue likely to prevent transformation, load, or core structure consistency",
30
+ "high": "Issue with high structural or migration impact requiring correction before safe continuation",
31
+ "medium": "Localized issue with limited migration impact, still requiring correction or review",
32
+ "low": "Signal worth surfacing but with low operational urgency",
33
+ }
34
+
35
+ SEVERITY_ORDER: Dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Issue Classification Dictionary — one entry per known finding_type
40
+ # ---------------------------------------------------------------------------
41
+ ISSUE_CLASSIFICATION_DICT: Dict[str, IssueClassificationEntry] = {
42
+ "cycle": IssueClassificationEntry(
43
+ issue_code="STR_CYCLE",
44
+ issue_label="Cycle detected",
45
+ issue_category="structural",
46
+ default_severity="critical",
47
+ default_priority=0,
48
+ default_resolution_category="business_decision_required",
49
+ requires_business_review=True,
50
+ structural_scope_type="subtree",
51
+ ),
52
+ "missing_reference": IssueClassificationEntry(
53
+ issue_code="STR_MISSING_REF",
54
+ issue_label="Missing reference",
55
+ issue_category="structural",
56
+ default_severity="high",
57
+ default_priority=1,
58
+ default_resolution_category="suggested_remediation",
59
+ requires_business_review=False,
60
+ structural_scope_type="multi_node",
61
+ ),
62
+ "self_reference": IssueClassificationEntry(
63
+ issue_code="STR_SELF_REF",
64
+ issue_label="Self-referencing relation",
65
+ issue_category="structural",
66
+ default_severity="high",
67
+ default_priority=1,
68
+ default_resolution_category="auto_fix",
69
+ requires_business_review=False,
70
+ structural_scope_type="local_node",
71
+ ),
72
+ "broken_link": IssueClassificationEntry(
73
+ issue_code="STR_BROKEN_LINK",
74
+ issue_label="Broken link",
75
+ issue_category="structural",
76
+ default_severity="high",
77
+ default_priority=1,
78
+ default_resolution_category="suggested_remediation",
79
+ requires_business_review=False,
80
+ structural_scope_type="local_node",
81
+ ),
82
+ "orphan_node": IssueClassificationEntry(
83
+ issue_code="STR_ORPHAN_NODE",
84
+ issue_label="Orphan node",
85
+ issue_category="structural",
86
+ default_severity="medium",
87
+ default_priority=2,
88
+ default_resolution_category="suggested_remediation",
89
+ requires_business_review=False,
90
+ structural_scope_type="local_node",
91
+ ),
92
+ "duplicate_relation": IssueClassificationEntry(
93
+ issue_code="STR_DUPLICATE_REL",
94
+ issue_label="Duplicate relation",
95
+ issue_category="structural",
96
+ default_severity="medium",
97
+ default_priority=2,
98
+ default_resolution_category="auto_fix",
99
+ requires_business_review=False,
100
+ structural_scope_type="local_node",
101
+ ),
102
+ }
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Fallback — used when finding_type is unknown or unmapped
107
+ # ---------------------------------------------------------------------------
108
+ FALLBACK_ENTRY = IssueClassificationEntry(
109
+ issue_code="UNK_UNCLASSIFIED",
110
+ issue_label="Unclassified issue",
111
+ issue_category="unclassified",
112
+ default_severity="medium",
113
+ default_priority=3,
114
+ default_resolution_category="suggested_remediation",
115
+ requires_business_review=False,
116
+ structural_scope_type="local_node",
117
+ )
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Helpers
122
+ # ---------------------------------------------------------------------------
123
+
124
+ def classify_finding(finding_type: str) -> IssueClassificationEntry:
125
+ """Look up classification for a finding_type. Falls back gracefully."""
126
+ return ISSUE_CLASSIFICATION_DICT.get(finding_type, FALLBACK_ENTRY)
127
+
128
+
129
+ def arbitrate_severity(finding_severity: str, dict_severity: str) -> str:
130
+ """Severity arbitration: highest severity wins.
131
+
132
+ Returns the more severe of the two levels.
133
+ """
134
+ f_rank = SEVERITY_ORDER.get(finding_severity, 99)
135
+ d_rank = SEVERITY_ORDER.get(dict_severity, 99)
136
+ # Lower rank = more severe
137
+ if f_rank <= d_rank:
138
+ return finding_severity
139
+ return dict_severity
140
+
141
+
142
+ def compute_priority(severity: str, scope_type: str) -> int:
143
+ """Derive priority from severity + scope.
144
+
145
+ Lower number = higher priority.
146
+ Wider scope at the same severity bumps priority up.
147
+ """
148
+ base = SEVERITY_ORDER.get(severity, 3)
149
+ scope_bonus = {
150
+ "structure_wide": -1,
151
+ "subtree": 0,
152
+ "multi_node": 0,
153
+ "local_node": 1,
154
+ }
155
+ return max(0, base + scope_bonus.get(scope_type, 0))
@@ -0,0 +1,177 @@
1
+ """
2
+ KnowledgePackLoader — charge et découpe les fichiers Markdown de 01_Dictionnaries
3
+ en chunks indexables pour le moteur BM25.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Domaines PLM — heuristique sur le nom de fichier (stem)
15
+ # ---------------------------------------------------------------------------
16
+ _DOMAIN_RULES: list[tuple[list[str], str]] = [
17
+ (["EBOM", "MBOM", "BoM", "CAD BOM", "Baseline"], "bill_of_materials"),
18
+ (["Change Management", "EIA-649", "CMII"], "change_management"),
19
+ (["DFMEA", "APQP", "CQCP", "CQCS", "IMR", "ISO 10005"], "quality"),
20
+ # bpmn/ handled separately via path
21
+ (["AML", "AVL", "ATO", "MTO", "MTS", "CTO", "ETO"], "supply_chain"),
22
+ (["ALM", "PDM"], "pdm"),
23
+ ]
24
+
25
+ _EXCLUDED_STEMS = {"BIM (Building Information Modeling)", "IaaS", "PaaS", "SaaS"}
26
+ _EXCLUDED_DIRS = {"rgpd", "data", "prompt_library"}
27
+
28
+
29
+ def _map_domain(path: Path) -> Optional[str]:
30
+ """Heuristique domaine à partir du chemin relatif."""
31
+ parts = [p.lower() for p in path.parts]
32
+ if "bpmn" in parts:
33
+ return "process_management"
34
+ if "ontology" in parts:
35
+ return "general_plm"
36
+ if "customers" in parts:
37
+ return "general_plm"
38
+ stem = path.stem
39
+ for keywords, domain in _DOMAIN_RULES:
40
+ for kw in keywords:
41
+ if kw.lower() in stem.lower():
42
+ return domain
43
+ return "general_plm"
44
+
45
+
46
+ def _slug(text: str) -> str:
47
+ return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Dataclass chunk
52
+ # ---------------------------------------------------------------------------
53
+ @dataclass
54
+ class KnowledgeChunk:
55
+ id: str # "{filename}#{section_slug}"
56
+ content: str # texte de la section
57
+ source_file: str # chemin relatif dans 01_Dictionnaries
58
+ section_title: str # titre du ## heading (ou "intro" pour contenu avant premier ##)
59
+ domain: Optional[str] = field(default=None)
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Loader
64
+ # ---------------------------------------------------------------------------
65
+ class KnowledgePackLoader:
66
+ """Charge les fichiers Markdown et les découpe par sections (## headings)."""
67
+
68
+ def __init__(self, dictionnaries_path: str = "../../01_Dictionnaries") -> None:
69
+ self.root = Path(dictionnaries_path).resolve()
70
+ self.chunks: list[KnowledgeChunk] = []
71
+
72
+ # ------------------------------------------------------------------
73
+ def load(self) -> list[KnowledgeChunk]:
74
+ """Charge tous les fichiers éligibles et retourne la liste de chunks."""
75
+ self.chunks = []
76
+ for path in self._get_relevant_files():
77
+ try:
78
+ new_chunks = self._chunk_markdown(path)
79
+ self.chunks.extend(new_chunks)
80
+ except Exception as exc: # noqa: BLE001
81
+ # Fichier illisible — on ignore sans bloquer
82
+ pass
83
+ return self.chunks
84
+
85
+ # ------------------------------------------------------------------
86
+ def _get_relevant_files(self) -> list[Path]:
87
+ if not self.root.exists():
88
+ return []
89
+
90
+ files: list[Path] = []
91
+
92
+ # Tier 1 : plm/, bpmn/, ontology/
93
+ for folder in ("plm", "bpmn", "ontology"):
94
+ folder_path = self.root / folder
95
+ if folder_path.is_dir():
96
+ for md in folder_path.glob("*.md"):
97
+ if self._is_included(md):
98
+ files.append(md)
99
+
100
+ # Tier 2 : customers/schneider-electrics/
101
+ se_root = self.root / "customers" / "schneider-electrics"
102
+ if se_root.is_dir():
103
+ # Fichier racine Schneider
104
+ for md in se_root.glob("*.md"):
105
+ if self._is_included(md):
106
+ files.append(md)
107
+ # Sous-dossier "3. Epics"
108
+ epics_dir = se_root / "3. Epics"
109
+ if epics_dir.is_dir():
110
+ for md in epics_dir.glob("*.md"):
111
+ if self._is_included(md):
112
+ files.append(md)
113
+
114
+ return files
115
+
116
+ # ------------------------------------------------------------------
117
+ def _is_included(self, path: Path) -> bool:
118
+ rel = path.relative_to(self.root)
119
+ parts = [p.lower() for p in rel.parts]
120
+ # Exclure répertoires interdits
121
+ for excl in _EXCLUDED_DIRS:
122
+ if excl in parts:
123
+ return False
124
+ # Exclure stems explicitement exclus (plm/BIM*.md, IaaS, PaaS, SaaS)
125
+ stem = path.stem
126
+ if stem in _EXCLUDED_STEMS:
127
+ return False
128
+ if stem.startswith("BIM"):
129
+ return False
130
+ # Exclure fichiers index/attachments peu utiles
131
+ if stem.lower() in {"attachments", "index"}:
132
+ return False
133
+ return True
134
+
135
+ # ------------------------------------------------------------------
136
+ def _chunk_markdown(self, path: Path) -> list[KnowledgeChunk]:
137
+ """Découpe un fichier .md en sections délimitées par des ## headings."""
138
+ rel = str(path.relative_to(self.root))
139
+ domain = _map_domain(path.relative_to(self.root))
140
+ text = path.read_text(encoding="utf-8", errors="replace")
141
+
142
+ chunks: list[KnowledgeChunk] = []
143
+ # Split sur les headings de niveau 2 (##)
144
+ sections = re.split(r"\n(?=## )", text)
145
+
146
+ for i, section in enumerate(sections):
147
+ lines = section.strip().splitlines()
148
+ if not lines:
149
+ continue
150
+
151
+ # Titre de section
152
+ if lines[0].startswith("## "):
153
+ title = lines[0][3:].strip()
154
+ body = "\n".join(lines[1:]).strip()
155
+ elif i == 0:
156
+ # Contenu avant le premier ## heading (intro / titre #)
157
+ # Prendre le premier # heading comme titre si présent
158
+ h1 = next((ln[2:].strip() for ln in lines if ln.startswith("# ")), path.stem)
159
+ title = h1
160
+ body = section.strip()
161
+ else:
162
+ title = path.stem
163
+ body = section.strip()
164
+
165
+ if not body or len(body) < 30:
166
+ continue
167
+
168
+ chunk_id = f"{_slug(path.stem)}#{_slug(title)}"
169
+ chunks.append(KnowledgeChunk(
170
+ id=chunk_id,
171
+ content=body[:4000], # cap à 4000 chars par chunk
172
+ source_file=rel,
173
+ section_title=title,
174
+ domain=domain,
175
+ ))
176
+
177
+ return chunks
@@ -0,0 +1,140 @@
1
+ """
2
+ KnowledgeSearchEngine — index BM25 sur les KnowledgeChunks PLM.
3
+
4
+ Trois alternatives de moteur de recherche ont été évaluées :
5
+ 1. BM25 (rank_bm25) — CHOIX RETENU
6
+ Avantages : dépendance ~10KB, démarrage instantané, excellent sur vocabulaire
7
+ technique précis (PLM, normes, acronymes). Pas de modèle à charger.
8
+ Limite : ne comprend pas les synonymes (ex: "validité" ≠ "effectivité").
9
+
10
+ 2. TF-IDF (scikit-learn)
11
+ Qualité similaire à BM25 sur texte structuré. ~50MB de dépendance (sklearn).
12
+ Démarrage ~1s. Envisageable si sklearn est déjà présent dans le projet.
13
+
14
+ 3. Embeddings sémantiques (sentence-transformers)
15
+ Comprend les synonymes et paraphrases. Modèle ~500MB à télécharger.
16
+ Démarrage 10-30s, RAM ~600MB. Recommandé si le corpus dépasse 1000 chunks
17
+ ou si les utilisateurs posent des questions en langage naturel non-technique.
18
+ Modèle suggéré : 'paraphrase-multilingual-MiniLM-L12-v2' (multilingue FR/EN).
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from typing import Optional
24
+
25
+ from plm_knowledge.knowledge_pack_loader import KnowledgeChunk, KnowledgePackLoader
26
+
27
+ try:
28
+ from rank_bm25 import BM25Okapi
29
+ _BM25_AVAILABLE = True
30
+ except ImportError:
31
+ _BM25_AVAILABLE = False
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Tokenizer simple (alphanumérique + accents FR)
36
+ # ---------------------------------------------------------------------------
37
+ def _tokenize(text: str) -> list[str]:
38
+ return re.findall(r"[a-zA-Z0-9\u00C0-\u024F]+", text.lower())
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Engine
43
+ # ---------------------------------------------------------------------------
44
+ class KnowledgeSearchEngine:
45
+ """
46
+ Index BM25 chargé en mémoire. Recherche par query + filtre domaine optionnel.
47
+ """
48
+
49
+ MIN_SCORE = 0.1 # Seuil de pertinence
50
+
51
+ def __init__(self, loader: KnowledgePackLoader) -> None:
52
+ self.chunks: list[KnowledgeChunk] = loader.chunks
53
+ self.is_ready = False
54
+ self._bm25: Optional[object] = None
55
+ self._tokenized: list[list[str]] = []
56
+
57
+ if not _BM25_AVAILABLE:
58
+ return
59
+ if not self.chunks:
60
+ return
61
+
62
+ self._tokenized = [_tokenize(c.content + " " + c.section_title) for c in self.chunks]
63
+ self._bm25 = BM25Okapi(self._tokenized)
64
+ self.is_ready = True
65
+
66
+ # ------------------------------------------------------------------
67
+ def search(
68
+ self,
69
+ query: str,
70
+ domain: Optional[str] = None,
71
+ top_k: int = 3,
72
+ ) -> list[KnowledgeChunk]:
73
+ """
74
+ Recherche les chunks les plus pertinents pour une query.
75
+
76
+ Args:
77
+ query: Texte de la requête (ex: "effectivité eBOM révision")
78
+ domain: Filtre domaine optionnel (ex: "bill_of_materials")
79
+ top_k: Nombre maximal de résultats
80
+
81
+ Returns:
82
+ Liste triée par score BM25 décroissant, filtrée par MIN_SCORE.
83
+ """
84
+ if not self.is_ready or not query:
85
+ return []
86
+
87
+ tokens = _tokenize(query)
88
+ if not tokens:
89
+ return []
90
+
91
+ scores = self._bm25.get_scores(tokens)
92
+
93
+ # Associer score + chunk
94
+ scored = list(zip(scores, self.chunks))
95
+
96
+ # Filtre domaine
97
+ if domain:
98
+ scored = [(s, c) for s, c in scored if c.domain == domain or c.domain is None]
99
+
100
+ # Seuil de pertinence
101
+ scored = [(s, c) for s, c in scored if s >= self.MIN_SCORE]
102
+
103
+ # Tri décroissant + top_k
104
+ scored.sort(key=lambda x: x[0], reverse=True)
105
+ return [c for _, c in scored[:top_k]]
106
+
107
+ # ------------------------------------------------------------------
108
+ def format_for_prompt(self, chunks: list[KnowledgeChunk]) -> str:
109
+ """
110
+ Formate les chunks en bloc texte prêt à être injecté dans un system prompt.
111
+
112
+ Format :
113
+ [Connaissances PLM pertinentes]
114
+ Source: plm/EBOM.md — ## Effectivité
115
+ <contenu...>
116
+ """
117
+ if not chunks:
118
+ return ""
119
+
120
+ lines = ["[Connaissances PLM pertinentes]"]
121
+ for chunk in chunks:
122
+ lines.append(f"\nSource: {chunk.source_file} — ## {chunk.section_title}")
123
+ lines.append(chunk.content)
124
+
125
+ return "\n".join(lines)
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Singleton (lazy-loaded)
130
+ # ---------------------------------------------------------------------------
131
+ _engine_instance: Optional[KnowledgeSearchEngine] = None
132
+
133
+
134
+ def set_engine(engine: KnowledgeSearchEngine) -> None:
135
+ global _engine_instance
136
+ _engine_instance = engine
137
+
138
+
139
+ def get_knowledge_engine() -> Optional[KnowledgeSearchEngine]:
140
+ return _engine_instance
@@ -0,0 +1,115 @@
1
+ """4-state lifecycle FSM for knowledge-pack manifests.
2
+
3
+ Aligned verbatim with PRD §8.11.3 capability lifecycle, mirroring the
4
+ Wave 6.5 Conv A `plm_skill_packages.lifecycle` precedent (Decision #167
5
+ / Q-W6.5A-2 = (a) "Align verbatim with PRD §8.11.3 4-state capability
6
+ lifecycle"). No new states, no knowledge-specific extensions, no
7
+ standalone divergence.
8
+
9
+ State chart::
10
+
11
+ draft ──► released ──► deprecated ──► retired
12
+ │
13
+ └──► (terminal)
14
+
15
+ States are **strictly forward-flowing**. The FSM rejects:
16
+ * any transition not in ``LIFECYCLE_TRANSITIONS``
17
+ * any move out of a terminal state
18
+ * skip transitions (e.g. ``draft → deprecated``)
19
+
20
+ Used by ``plm_knowledge.registry.KnowledgePackRegistry`` to gate
21
+ manifest publishing + by future admin tools (V1.1+) to mediate
22
+ state-change audit events.
23
+
24
+ Wave 6.5 Conv B constraint: this module is the SOLE lifecycle FSM
25
+ for knowledge-pack manifests. The shared 4-state contract sits in
26
+ ``plm_shared.capability_registry`` (PRD §8.11.3) and is mirrored
27
+ here verbatim — string identifiers chosen to match for cross-checks
28
+ in ``tests/test_lifecycle.py``.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from typing import Tuple
33
+
34
+ # ── Canonical state identifiers (PRD §8.11.3 verbatim) ─────────────────────
35
+
36
+ STATE_DRAFT: str = "draft"
37
+ STATE_RELEASED: str = "released"
38
+ STATE_DEPRECATED: str = "deprecated"
39
+ STATE_RETIRED: str = "retired"
40
+
41
+ LIFECYCLE_STATES: Tuple[str, ...] = (
42
+ STATE_DRAFT,
43
+ STATE_RELEASED,
44
+ STATE_DEPRECATED,
45
+ STATE_RETIRED,
46
+ )
47
+
48
+ # ── Allowed transitions (forward-flowing only) ─────────────────────────────
49
+ #
50
+ # Each tuple is (from, to). Skips are rejected (no draft→deprecated). Out-
51
+ # of-terminal moves are rejected (no retired→anywhere).
52
+
53
+ LIFECYCLE_TRANSITIONS: Tuple[Tuple[str, str], ...] = (
54
+ (STATE_DRAFT, STATE_RELEASED),
55
+ (STATE_RELEASED, STATE_DEPRECATED),
56
+ (STATE_DEPRECATED, STATE_RETIRED),
57
+ )
58
+
59
+ TERMINAL_STATES: Tuple[str, ...] = (STATE_RETIRED,)
60
+
61
+
62
+ class InvalidLifecycleTransition(ValueError):
63
+ """Raised on any state move that is not in ``LIFECYCLE_TRANSITIONS``.
64
+
65
+ Carries the attempted (from_state, to_state) pair so callers
66
+ (e.g. registry publishing flow) can emit a precise audit event.
67
+ """
68
+
69
+ def __init__(self, from_state: str, to_state: str) -> None:
70
+ self.from_state = from_state
71
+ self.to_state = to_state
72
+ super().__init__(
73
+ f"invalid lifecycle transition: {from_state!r} → {to_state!r} "
74
+ f"(allowed: {sorted(LIFECYCLE_TRANSITIONS)})"
75
+ )
76
+
77
+
78
+ def is_terminal(state: str) -> bool:
79
+ """Return True iff ``state`` is a terminal state (no outbound transitions)."""
80
+ return state in TERMINAL_STATES
81
+
82
+
83
+ def can_transition(from_state: str, to_state: str) -> bool:
84
+ """Return True iff ``(from_state, to_state)`` is in ``LIFECYCLE_TRANSITIONS``."""
85
+ return (from_state, to_state) in LIFECYCLE_TRANSITIONS
86
+
87
+
88
+ def next_state(from_state: str, to_state: str) -> str:
89
+ """Apply the transition and return ``to_state``; raise on invalid moves.
90
+
91
+ Pure function — does NOT persist; the caller (registry) writes the
92
+ new state into the manifest store after this returns successfully.
93
+ """
94
+ if from_state not in LIFECYCLE_STATES:
95
+ raise InvalidLifecycleTransition(from_state, to_state)
96
+ if to_state not in LIFECYCLE_STATES:
97
+ raise InvalidLifecycleTransition(from_state, to_state)
98
+ if not can_transition(from_state, to_state):
99
+ raise InvalidLifecycleTransition(from_state, to_state)
100
+ return to_state
101
+
102
+
103
+ __all__ = [
104
+ "STATE_DRAFT",
105
+ "STATE_RELEASED",
106
+ "STATE_DEPRECATED",
107
+ "STATE_RETIRED",
108
+ "LIFECYCLE_STATES",
109
+ "LIFECYCLE_TRANSITIONS",
110
+ "TERMINAL_STATES",
111
+ "InvalidLifecycleTransition",
112
+ "is_terminal",
113
+ "can_transition",
114
+ "next_state",
115
+ ]
@@ -0,0 +1,17 @@
1
+ pack_id: foundation.dictionnaries
2
+ kind: foundation_dictionary
3
+ title: Foundation — 01_Dictionnaries Markdown corpus
4
+ description: 146 PLM domain markdown files at 01_Dictionnaries/ (path resolved via plm_knowledge.foundation_loader; physical relocation deferred to Wave 7 per Decision #171).
5
+ version: 1.0.0
6
+ lifecycle_state: released
7
+ capability_id: knowledge.foundation.dictionnaries
8
+ entry_module: plm_knowledge.foundation_loader
9
+ entry_callable: list_foundation_documents
10
+ tags:
11
+ - foundation
12
+ - markdown
13
+ - dictionnaries
14
+ notes: |
15
+ Wave 6.5 Conv B carve: directory stays at repo root through Wave 6.5; the
16
+ thin loader resolves the path through backend `config.dictionnaries_path`.
17
+ Physical relocation lands at Wave 7 Extraction.
@@ -0,0 +1,16 @@
1
+ pack_id: foundation.evaluation_grids
2
+ kind: foundation_evaluation_grid
3
+ title: Foundation — PLM evaluation grids
4
+ description: PLM evaluation rubrics consumed by the business validator + quality gate; resolved through the Foundation root path resolver.
5
+ version: 1.0.0
6
+ lifecycle_state: released
7
+ capability_id: knowledge.foundation.evaluation_grids
8
+ entry_module: plm_knowledge.foundation_loader
9
+ entry_callable: list_foundation_documents
10
+ tags:
11
+ - foundation
12
+ - evaluation
13
+ - rubric
14
+ notes: |
15
+ V1 sources rubrics from the shared 01_Dictionnaries tree (filtered subset).
16
+ V1.1+ may split into a dedicated foundation/grids/ subdirectory.
@@ -0,0 +1,15 @@
1
+ pack_id: foundation.taxonomy
2
+ kind: foundation_taxonomy
3
+ title: Foundation — PLM taxonomy
4
+ description: PLM domain taxonomy mappings (e.g. EBOM/MBOM/CAD-BOM, Change Management, Quality, Supply Chain) used by the BM25 loader to assign domain tags.
5
+ version: 1.0.0
6
+ lifecycle_state: released
7
+ capability_id: knowledge.foundation.taxonomy
8
+ entry_module: plm_knowledge.knowledge_pack_loader
9
+ entry_callable: KnowledgePackLoader
10
+ tags:
11
+ - foundation
12
+ - taxonomy
13
+ notes: |
14
+ V1 surface lives inside knowledge_pack_loader's _DOMAIN_RULES heuristic.
15
+ V1.1+ promotes this to a standalone declarative file under foundation/.
@@ -0,0 +1,18 @@
1
+ pack_id: operational.placeholder
2
+ kind: operational
3
+ title: Operational — V1 placeholder
4
+ description: V1 operational layer placeholder per D-LOCKED-15 + Decision #172. Stable but inert public surface so downstream callers can wire against the contract today.
5
+ version: 1.0.0
6
+ lifecycle_state: draft
7
+ capability_id: knowledge.operational.placeholder
8
+ entry_module: plm_knowledge.operational
9
+ entry_callable: record_operational_event
10
+ tags:
11
+ - operational
12
+ - placeholder
13
+ notes: |
14
+ V1 (Wave 6.5 Conv B): no-op — events are logged at INFO and
15
+ discarded. Lifecycle state intentionally `draft` to flag that the
16
+ V1.1+ implementation is the genuine release path. Decision #172
17
+ preserves in-process state at V1; persistence + analytics surface
18
+ lands via a dedicated story.
@@ -0,0 +1,20 @@
1
+ pack_id: retrieval.bm25
2
+ kind: retrieval_bm25
3
+ title: Retrieval — BM25 search engine
4
+ description: BM25 (rank_bm25) search engine over PLM KnowledgeChunks loaded from 01_Dictionnaries/. Module-level singleton accessed via set_engine + get_knowledge_engine.
5
+ version: 1.0.0
6
+ lifecycle_state: released
7
+ capability_id: knowledge.retrieval.bm25
8
+ autonomy_level: supervised
9
+ entry_module: plm_knowledge.knowledge_search_engine
10
+ entry_callable: get_knowledge_engine
11
+ tags:
12
+ - retrieval
13
+ - bm25
14
+ - search
15
+ notes: |
16
+ Lifted from 02_App/backend/skills/knowledge_search_engine.py +
17
+ 02_App/backend/skills/knowledge_pack_loader.py via git mv at Wave 6.5
18
+ Conv B. Re-export shims preserve legacy `skills.knowledge_*` imports
19
+ including the module-level singleton (set_engine + get_knowledge_engine
20
+ point at the canonical singleton in plm_knowledge).