@simbimbo/brainstem 0.0.3 → 0.0.5

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.5 — 2026-03-22
4
+
5
+ Source capability reporting and docs/runtime alignment pass.
6
+
7
+ ### Highlights
8
+ - exposes compact source-support matrix in `/runtime` (`capability_flags.source_capabilities`)
9
+ - documents LogicMonitor-shaped ingest path as implemented runtime API surface
10
+ - aligns README/docs/examples with runtime API surface and active source drivers
11
+ - keeps behavior intact for existing ingestion/canonicalization paths
12
+
13
+ ### Validation
14
+ - local test suite passed (with updated runtime capability coverage)
15
+
16
+ ## 0.0.4 — 2026-03-22
17
+
18
+ Release-prep alignment for the current intake foundation state.
19
+
20
+ ### Highlights
21
+ - aligns packaging/runtime version metadata for the next release cut (`0.0.4` in `package.json`, `pyproject.toml`, and `brainstem.__version__`)
22
+ - updates README/API-facing documentation to call out only implemented runtime and listener surfaces
23
+ - keeps claims bounded to implemented behavior; no new feature claims
24
+
25
+ ### Notes
26
+ - no runtime behavior or interface changes in this release-prep pass
27
+ - no publish action performed
28
+
3
29
  ## 0.0.3 — 2026-03-22
4
30
 
5
31
  Intake Foundation follow-up release for **brAInstem**.
package/README.md CHANGED
@@ -48,6 +48,32 @@ For a given tenant or environment, brAInstem should answer:
48
48
  - Have we seen this before?
49
49
  - What happened right before the last similar incident?
50
50
 
51
+ ## Current implemented runtime surface
52
+
53
+ Current implementation is intentionally bounded:
54
+ - FastAPI runtime at `brainstem.api:app`
55
+ - Implemented API endpoints include:
56
+ - `POST /ingest/event`, `POST /ingest/batch`, `POST /ingest/logicmonitor`, `POST /replay/raw`
57
+ - `GET /interesting`, `GET /candidates`, `GET /signatures`, `GET /canonical_events`
58
+ - `GET /stats`, `GET /failures`, `GET /failures/{id}`
59
+ - `GET /raw_envelopes`, `GET /ingest/recent`, `GET /sources`, `GET /sources/status`
60
+ - `GET /runtime`, `GET /status`, `GET /healthz`
61
+ - UDP syslog listener at `brainstem.listener`
62
+ - source-driver intake for `syslog`, `file`, and narrow `logicmonitor` connector payload shape
63
+ - SQLite-backed persistence with default DB at `.brainstem-state/brainstem.sqlite3`
64
+ - optional API token in `BRAINSTEM_API_TOKEN`
65
+
66
+ Runtime config can also be inspected at `/runtime`. The runtime summary now includes explicit `source_capabilities`
67
+ (`source_types` + per-source ingest modes) and auth/capabilities flags.
68
+
69
+ ## Quick run path (API + listener + file/syslog intake)
70
+
71
+ See the compact run surface in:
72
+
73
+ - [`docs/README.md`](docs/README.md)
74
+
75
+ It covers API startup, listener ingest, and file ingest end-to-end with only implemented entry points and current payload shapes.
76
+
51
77
  ## Relationship to ocmemog
52
78
 
53
79
  Shared DNA:
@@ -1,3 +1,3 @@
1
1
  """brAInstem — operational memory for weak signals."""
2
2
 
3
- __version__ = "0.0.3"
3
+ __version__ = "0.0.5"
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Any, Dict, Protocol, runtime_checkable
6
+
7
+ from .models import RawInputEnvelope
8
+
9
+
10
+ @runtime_checkable
11
+ class RawInputAdapter(Protocol):
12
+ """Contract for source-specific envelope producers."""
13
+
14
+ source_type: str
15
+
16
+ def parse_raw_input(
17
+ self,
18
+ payload: Any,
19
+ *,
20
+ tenant_id: str,
21
+ source_path: str = "",
22
+ ) -> RawInputEnvelope:
23
+ """Build a RawInputEnvelope from source payload."""
24
+
25
+
26
+ _ADAPTER_REGISTRY: Dict[str, RawInputAdapter] = {}
27
+
28
+
29
+ def register_raw_input_adapter(adapter: RawInputAdapter) -> None:
30
+ """Register/replace an adapter for a source type."""
31
+
32
+ _ADAPTER_REGISTRY[adapter.source_type] = adapter
33
+
34
+
35
+ def get_raw_input_adapter(source_type: str) -> RawInputAdapter:
36
+ """Return the adapter for `source_type`."""
37
+
38
+ try:
39
+ return _ADAPTER_REGISTRY[source_type]
40
+ except KeyError as exc:
41
+ raise ValueError(f"unsupported source_type: {source_type}") from exc
42
+
43
+
44
+ def list_raw_input_source_types() -> list[str]:
45
+ """Return currently registered source types (inspectable list)."""
46
+
47
+ return sorted(_ADAPTER_REGISTRY.keys())
48
+
49
+
50
+ def build_raw_input_envelope(
51
+ source_type: str,
52
+ payload: Any,
53
+ *,
54
+ tenant_id: str,
55
+ source_path: str = "",
56
+ ) -> RawInputEnvelope:
57
+ """Build a RawInputEnvelope using the source-type registry."""
58
+
59
+ adapter = get_raw_input_adapter(source_type)
60
+ return adapter.parse_raw_input(payload, tenant_id=tenant_id, source_path=source_path)
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class SyslogRawInputAdapter:
65
+ """Simple adapter for syslog-like line-oriented payloads."""
66
+
67
+ source_type: str = "syslog"
68
+
69
+ def parse_raw_input(self, payload: Any, *, tenant_id: str, source_path: str = "") -> RawInputEnvelope:
70
+ text = ("" if payload is None else str(payload)).rstrip("\n")
71
+ timestamp = datetime.utcnow().isoformat() + "Z"
72
+ host = ""
73
+ service = ""
74
+ message = text
75
+
76
+ parts = text.split()
77
+ if len(parts) >= 5:
78
+ host = parts[3]
79
+ rest = " ".join(parts[4:])
80
+ if ":" in rest:
81
+ svc, _, msg = rest.partition(":")
82
+ service = svc.strip()
83
+ message = msg.strip() or rest.strip()
84
+ else:
85
+ message = rest.strip()
86
+
87
+ return RawInputEnvelope(
88
+ tenant_id=tenant_id,
89
+ source_type=self.source_type,
90
+ timestamp=timestamp,
91
+ message_raw=message,
92
+ host=host,
93
+ service=service,
94
+ source_path=source_path,
95
+ metadata={"raw_line": text},
96
+ )
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class FileRawInputAdapter:
101
+ """Simple adapter for generic line-oriented log files."""
102
+
103
+ source_type: str = "file"
104
+
105
+ def parse_raw_input(self, payload: Any, *, tenant_id: str, source_path: str = "") -> RawInputEnvelope:
106
+ text = "" if payload is None else str(payload)
107
+ timestamp = datetime.utcnow().isoformat() + "Z"
108
+
109
+ return RawInputEnvelope(
110
+ tenant_id=tenant_id,
111
+ source_type=self.source_type,
112
+ timestamp=timestamp,
113
+ message_raw=text.rstrip("\n"),
114
+ source_path=source_path,
115
+ metadata={"raw_line": text},
116
+ )
117
+
118
+
119
+ register_raw_input_adapter(SyslogRawInputAdapter())
120
+ register_raw_input_adapter(FileRawInputAdapter())