77459d06d6
Two reviewed knowledge decisions (2026-06-28) + the deferred cosmetic counter, before #59. 1. ISO13485 removed from the incident_management hypothesis. ISO 13485 CAPA / quality-safety incident handling is NOT security incident management — the mapping was too broad and would seed false hypotheses for the empirical loop. A dedicated manage_quality_and_safety_incidents capability can come later IF a target needs it; not forced now. (ISO27001/TISAX/IEC62443 keep incident_management.) 2. patch_policy_doc -> secure_signed_update_distribution stays `partial`, but the curated rationale is sharpened: "indicates update governance, does not evidence signed distribution" (a patch policy is not proof of SIGNED distribution). New optional SignalMapping.rationale field carries the curated note. (github_actions_ci -> SDL and dependency_scanning -> vuln-mgmt reviewed and APPROVED as-is.) 3. Cosmetic (folded in since we touched the file): the silent-intake summary now counts detected and indications SEPARATELY ("N automatisch erkannt, M Indikation(en)") instead of lumping partial signals into "automatisch erkannt" — consistent with the three-state model just shipped. Tests: ISO13485 no longer resolves to incident_management; summary counts split correctly. 29 onboarding tests pass, mypy --strict clean, demo runs, check-loc 0. Runtime-visible (hypothesis resolution + summary text) -> deploy + smoke.
125 lines
6.7 KiB
Python
125 lines
6.7 KiB
Python
"""Silent Knowledge Pass — recognise everything possible BEFORE asking a single question (Phase 0).
|
|
|
|
The Advisor can say "I need 5 answers" but does not yet decide WHAT it can find out by itself. The Silent
|
|
Pass runs first: from signals that existing scanners/parsers already produce (website, repository,
|
|
documents, product data) it deterministically derives capabilities the company demonstrably HAS and
|
|
product facts that drive scope — so every recognised item shrinks the delta and removes a question.
|
|
|
|
The customer then experiences "we already recognised 11 of 17 — only these 4 remain" instead of a
|
|
question wall. This is NOT new architecture: it is one orchestration step in front of the Advisor
|
|
Company -> Silent Intake -> Company Profile -> Hypotheses -> Delta -> Top Questions
|
|
All building blocks already exist. SIGNALS are INJECTED (the scanners produce them); the signal->capability
|
|
map is curated DATA, also injected. Pure, deterministic, no I/O. Python 3.9 compatible.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, List, Optional, Sequence, Set
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class IntakeSignal(BaseModel):
|
|
"""A CANONICAL signal the Silent Pass consumes. Producer-agnostic: the same `signal` may have come
|
|
from a website, a repo, a PDF, a tender or the user — normalize_signals() unified them (see signals.py)."""
|
|
|
|
source: str # source_type: website / repository / document / product / tender / user
|
|
signal: str # CANONICAL signal id, e.g. "sbom_present"
|
|
kind: str = "observation" # "observation" (I saw X) | "requirement" (someone DEMANDS X)
|
|
confidence: float = 1.0 # carried from the producer
|
|
evidence: Optional[str] = None # the artifact already in hand
|
|
provenance: str = "" # where it came from (url / filename / tender clause) — audit trail
|
|
detail: str = "" # free-text (kept for back-compat)
|
|
|
|
|
|
class SignalMapping(BaseModel):
|
|
"""Curated: what a signal lets us conclude. A signal yields a capability OR a product fact."""
|
|
|
|
signal: str
|
|
capability: Optional[str] = None # capability the signal evidences
|
|
relationship: str = "detected" # detected (concrete artifact) / partial (indicative)
|
|
evidence: Optional[str] = None # the artifact found (already in hand -> no upload needed)
|
|
product_fact: Optional[str] = None # e.g. "connected_to_internet"
|
|
fact_value: str = "true"
|
|
rationale: str = "" # curated note: WHY only indicative (esp. for partial mappings)
|
|
|
|
|
|
class DetectedCapability(BaseModel):
|
|
capability: str
|
|
relationship: str = "detected"
|
|
source: str = "" # which signal/source detected it (audit trail)
|
|
evidence: Optional[str] = None
|
|
confidence: float = 1.0 # carried from the producing signal
|
|
provenance: str = "" # where the signal came from
|
|
|
|
|
|
class ProductFact(BaseModel):
|
|
key: str
|
|
value: str = "true"
|
|
source: str = ""
|
|
|
|
|
|
class SilentIntakeResult(BaseModel):
|
|
detected_capabilities: List[DetectedCapability] = Field(default_factory=list)
|
|
product_facts: List[ProductFact] = Field(default_factory=list)
|
|
evidence_found: List[str] = Field(default_factory=list)
|
|
requirements_seen: List[str] = Field(default_factory=list) # requirement-kind signals — preserved, NOT present
|
|
summary: str = ""
|
|
|
|
def capability_ids(self) -> List[str]:
|
|
"""The DETECTED capability ids (relationship == detected) — fed into the Advisor as already-present
|
|
(delta-reducing, not asked). ONLY observation-kind signals reach here (requirements never become a
|
|
present capability); a merely PARTIAL/indicative signal does NOT (see indicative_capability_ids)."""
|
|
return sorted({d.capability for d in self.detected_capabilities if d.relationship == "detected"})
|
|
|
|
def indicative_capability_ids(self) -> List[str]:
|
|
"""Capabilities backed only by a PARTIAL/indicative signal — they raise assumption strength but do
|
|
NOT replace a question (the gap stays open and is still asked, just with an indication shown)."""
|
|
return sorted({d.capability for d in self.detected_capabilities if d.relationship != "detected"})
|
|
|
|
|
|
def silent_intake(
|
|
signals: Sequence[IntakeSignal], signal_map: Sequence[SignalMapping]
|
|
) -> SilentIntakeResult:
|
|
"""Derive capabilities + product facts from injected scanner signals (deterministic, no questions).
|
|
|
|
Each signal is matched to curated mappings by `signal` id; a mapping contributes either a detected
|
|
capability (+ optional evidence already in hand) or a product fact. Deduped, deterministic order.
|
|
"""
|
|
by_signal: Dict[str, List[SignalMapping]] = {}
|
|
for m in signal_map:
|
|
by_signal.setdefault(m.signal, []).append(m)
|
|
|
|
caps: Dict[str, DetectedCapability] = {}
|
|
facts: Dict[str, ProductFact] = {}
|
|
evidence: Set[str] = set()
|
|
requirements: Set[str] = set()
|
|
for s in signals:
|
|
if s.kind != "observation": # a requirement describes a TARGET, never the present state
|
|
requirements.add(s.signal) # preserved + visible, but NEVER turned into a capability
|
|
continue
|
|
for m in by_signal.get(s.signal, []):
|
|
if m.capability and m.capability not in caps:
|
|
caps[m.capability] = DetectedCapability(
|
|
capability=m.capability, relationship=m.relationship,
|
|
source="%s:%s" % (s.source, s.signal), evidence=m.evidence,
|
|
confidence=s.confidence, provenance=s.provenance)
|
|
if m.evidence:
|
|
evidence.add(m.evidence)
|
|
if m.product_fact:
|
|
facts[m.product_fact] = ProductFact(key=m.product_fact, value=m.fact_value, source=s.source)
|
|
|
|
detected = [caps[k] for k in sorted(caps)]
|
|
product_facts = [facts[k] for k in sorted(facts)]
|
|
requirements_seen = sorted(requirements)
|
|
n_detected = sum(1 for d in detected if d.relationship == "detected") # concrete artifacts -> auto-detected
|
|
n_indication = len(detected) - n_detected # partial -> indication, still asked
|
|
summary = (
|
|
"Stille Vorbefüllung: %d Fähigkeit(en) automatisch erkannt, %d Indikation(en), %d Produktfakt(en), "
|
|
"%d Nachweis(e) bereits vorhanden, %d Anforderung(en) erkannt (nicht als vorhanden gewertet)."
|
|
% (n_detected, n_indication, len(product_facts), len(evidence), len(requirements_seen))
|
|
)
|
|
return SilentIntakeResult(
|
|
detected_capabilities=detected, product_facts=product_facts,
|
|
evidence_found=sorted(evidence), requirements_seen=requirements_seen, summary=summary)
|