sylo-fieldbrain 0.1.0

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/extensions/fieldbrain-tools.ts +495 -0
  4. package/extensions/index.ts +12 -0
  5. package/extensions/python-runner.ts +56 -0
  6. package/package.json +44 -0
  7. package/scripts/__pycache__/_db_lib.cpython-312.pyc +0 -0
  8. package/scripts/__pycache__/_json_out.cpython-312.pyc +0 -0
  9. package/scripts/__pycache__/models.cpython-312.pyc +0 -0
  10. package/scripts/_db_lib.py +228 -0
  11. package/scripts/_json_out.py +19 -0
  12. package/scripts/alembic/env.py +53 -0
  13. package/scripts/alembic/versions/001_baseline.py +38 -0
  14. package/scripts/alembic/versions/002_core_tables.py +429 -0
  15. package/scripts/alembic/versions/003_durability_content_in_db.py +106 -0
  16. package/scripts/alembic/versions/004_pgvector_embedding.py +42 -0
  17. package/scripts/alembic/versions/005_project_job_number.py +37 -0
  18. package/scripts/alembic/versions/006_project_parent.py +62 -0
  19. package/scripts/alembic/versions/__init__.py +1 -0
  20. package/scripts/alembic.ini +38 -0
  21. package/scripts/db_auto_migrate.py +103 -0
  22. package/scripts/db_bootstrap.py +207 -0
  23. package/scripts/db_check.py +105 -0
  24. package/scripts/db_migrate.py +85 -0
  25. package/scripts/fieldbrain_brain_delete.py +49 -0
  26. package/scripts/fieldbrain_brain_read.py +61 -0
  27. package/scripts/fieldbrain_brain_restore.py +49 -0
  28. package/scripts/fieldbrain_brain_revisions.py +54 -0
  29. package/scripts/fieldbrain_brain_write.py +67 -0
  30. package/scripts/fieldbrain_document_attach.py +81 -0
  31. package/scripts/fieldbrain_document_catalog.py +128 -0
  32. package/scripts/fieldbrain_document_ingest.py +62 -0
  33. package/scripts/fieldbrain_document_list.py +67 -0
  34. package/scripts/fieldbrain_document_promote.py +43 -0
  35. package/scripts/fieldbrain_log_create.py +55 -0
  36. package/scripts/fieldbrain_log_delete.py +39 -0
  37. package/scripts/fieldbrain_log_restore.py +39 -0
  38. package/scripts/fieldbrain_log_revisions.py +39 -0
  39. package/scripts/fieldbrain_log_search.py +58 -0
  40. package/scripts/fieldbrain_log_update.py +52 -0
  41. package/scripts/fieldbrain_project_create.py +72 -0
  42. package/scripts/fieldbrain_project_list.py +53 -0
  43. package/scripts/fieldbrain_search.py +74 -0
  44. package/scripts/fieldbrain_ui_brain_list.py +60 -0
  45. package/scripts/fieldbrain_ui_project_create.py +95 -0
  46. package/scripts/fieldbrain_ui_project_list.py +49 -0
  47. package/scripts/models.py +355 -0
  48. package/scripts/pgvector_enable.py +165 -0
  49. package/scripts/pgvector_guide.py +44 -0
  50. package/scripts/pgvector_install_files.py +66 -0
  51. package/scripts/pgvector_install_from_folder.py +148 -0
  52. package/scripts/postbuild-ui.mjs +13 -0
  53. package/scripts/requirements.txt +7 -0
  54. package/scripts/services/__init__.py +1 -0
  55. package/scripts/services/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/scripts/services/__pycache__/brain_paths.cpython-312.pyc +0 -0
  57. package/scripts/services/__pycache__/document_formats.cpython-312.pyc +0 -0
  58. package/scripts/services/__pycache__/document_service.cpython-312.pyc +0 -0
  59. package/scripts/services/__pycache__/pgvector_windows.cpython-312.pyc +0 -0
  60. package/scripts/services/__pycache__/project_naming.cpython-312.pyc +0 -0
  61. package/scripts/services/__pycache__/project_service.cpython-312.pyc +0 -0
  62. package/scripts/services/brain_paths.py +81 -0
  63. package/scripts/services/brain_service.py +280 -0
  64. package/scripts/services/document_catalog.py +186 -0
  65. package/scripts/services/document_formats.py +116 -0
  66. package/scripts/services/document_ingest.py +254 -0
  67. package/scripts/services/document_service.py +316 -0
  68. package/scripts/services/embedding_service.py +72 -0
  69. package/scripts/services/global_brain_support.py +36 -0
  70. package/scripts/services/maintenance_log_service.py +258 -0
  71. package/scripts/services/migrate_lock.py +24 -0
  72. package/scripts/services/pgvector_windows.py +190 -0
  73. package/scripts/services/project_naming.py +72 -0
  74. package/scripts/services/project_service.py +368 -0
  75. package/scripts/services/search_service.py +250 -0
  76. package/scripts/status.py +39 -0
  77. package/shared/README.md +7 -0
  78. package/skills/fieldbrain/SKILL.md +197 -0
  79. package/skills/fieldbrain/SKILL.md.bak +83 -0
  80. package/skills/fieldbrain/routes/fieldbrain/assets/index-B56utPpP.css +1 -0
  81. package/skills/fieldbrain/routes/fieldbrain/assets/index-DO0AD0df.js +55 -0
  82. package/skills/fieldbrain/routes/fieldbrain/fallback.md +7 -0
  83. package/skills/fieldbrain/routes/fieldbrain/index.html +13 -0
@@ -0,0 +1,250 @@
1
+ """Hybrid search: keyword tsvector + optional pgvector cosine via simplified RRF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from sqlalchemy import text
10
+ from sqlalchemy.orm import Session
11
+
12
+ from _db_lib import check_pgvector, ollama_base_url
13
+ from models import GlobalSettings
14
+ from services.embedding_service import generate_single_embedding
15
+ from services.global_brain_support import get_global_brain_shell_project_id
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ KEYWORD_WEIGHT = 0.7
20
+ SEMANTIC_WEIGHT = 0.3
21
+ RRF_K = 60
22
+ SNIPPET_CHARS = 300
23
+ SOURCE_DOCUMENT = "document"
24
+ SOURCE_BRAIN = "brain"
25
+
26
+
27
+ @dataclass
28
+ class SearchHit:
29
+ id: int
30
+ project_id: int
31
+ source_type: str
32
+ source_id: str | None
33
+ label: str
34
+ path: str
35
+ content: str
36
+ chunk_index: int = 0
37
+
38
+
39
+ def _get_embedding_model(session: Session) -> str | None:
40
+ settings = session.query(GlobalSettings).filter(GlobalSettings.id == 1).first()
41
+ if settings and settings.embedding_model and settings.embedding_model.strip():
42
+ return settings.embedding_model.strip()
43
+ return "nomic-embed-text"
44
+
45
+
46
+ def _keyword_search(
47
+ session: Session,
48
+ project_ids: list[int],
49
+ q: str,
50
+ limit: int,
51
+ *,
52
+ source_type: str | None = None,
53
+ ) -> list[SearchHit]:
54
+ filter_sql = ""
55
+ params: dict[str, Any] = {"pids": project_ids, "q": q.strip(), "lim": limit}
56
+ if source_type and source_type.strip():
57
+ filter_sql = " AND source_type = :stype"
58
+ params["stype"] = source_type.strip()
59
+
60
+ rows = session.execute(
61
+ text(
62
+ f"""
63
+ SELECT id, project_id, source_type, source_id, label, path, content, chunk_index
64
+ FROM search_index
65
+ WHERE project_id = ANY(:pids)
66
+ AND search_vector @@ plainto_tsquery('english', :q)
67
+ {filter_sql}
68
+ ORDER BY ts_rank_cd(
69
+ '{{0.1, 0.2, 0.4, 1.0}}'::float4[],
70
+ search_vector,
71
+ plainto_tsquery('english', :q)
72
+ ) DESC
73
+ LIMIT :lim
74
+ """
75
+ ),
76
+ params,
77
+ ).fetchall()
78
+
79
+ return [
80
+ SearchHit(
81
+ id=row[0],
82
+ project_id=int(row[1]),
83
+ source_type=row[2],
84
+ source_id=row[3],
85
+ label=row[4] or "",
86
+ path=row[5] or "",
87
+ content=row[6] or "",
88
+ chunk_index=int(row[7] or 0),
89
+ )
90
+ for row in rows
91
+ ]
92
+
93
+
94
+ def _semantic_search(
95
+ session: Session,
96
+ project_ids: list[int],
97
+ embedding: list[float],
98
+ limit: int,
99
+ *,
100
+ source_type: str | None = None,
101
+ ) -> list[SearchHit]:
102
+ emb_str = "[" + ",".join(str(v) for v in embedding) + "]"
103
+ filter_sql = ""
104
+ params: dict[str, Any] = {"pids": project_ids, "emb": emb_str, "lim": limit}
105
+ if source_type and source_type.strip():
106
+ filter_sql = " AND source_type = :stype"
107
+ params["stype"] = source_type.strip()
108
+
109
+ try:
110
+ rows = session.execute(
111
+ text(
112
+ f"""
113
+ SELECT id, project_id, source_type, source_id, label, path, content, chunk_index
114
+ FROM search_index
115
+ WHERE project_id = ANY(:pids)
116
+ AND embedding IS NOT NULL
117
+ {filter_sql}
118
+ ORDER BY embedding <=> CAST(:emb AS vector)
119
+ LIMIT :lim
120
+ """
121
+ ),
122
+ params,
123
+ ).fetchall()
124
+ except Exception as exc:
125
+ err = str(exc).lower()
126
+ if "embedding" in err and "does not exist" in err:
127
+ logger.warning("Semantic search skipped: no embedding column (%s)", exc)
128
+ else:
129
+ logger.warning("Semantic search failed: %s", exc)
130
+ session.rollback()
131
+ return []
132
+
133
+ return [
134
+ SearchHit(
135
+ id=row[0],
136
+ project_id=int(row[1]),
137
+ source_type=row[2],
138
+ source_id=row[3],
139
+ label=row[4] or "",
140
+ path=row[5] or "",
141
+ content=row[6] or "",
142
+ chunk_index=int(row[7] or 0),
143
+ )
144
+ for row in rows
145
+ ]
146
+
147
+
148
+ def _snippet(content: str, max_chars: int = SNIPPET_CHARS) -> str:
149
+ c = (content or "").strip()
150
+ if len(c) <= max_chars:
151
+ return c
152
+ return c[:max_chars].strip()
153
+
154
+
155
+ def _fuse_rrf(
156
+ keyword_hits: list[SearchHit],
157
+ semantic_hits: list[SearchHit],
158
+ ) -> list[tuple[SearchHit, float]]:
159
+ kw_rank = {h.id: i for i, h in enumerate(keyword_hits)}
160
+ sem_rank = {h.id: i for i, h in enumerate(semantic_hits)}
161
+ all_ids = set(kw_rank) | set(sem_rank)
162
+
163
+ hit_by_id: dict[int, SearchHit] = {}
164
+ for h in keyword_hits:
165
+ hit_by_id[h.id] = h
166
+ for h in semantic_hits:
167
+ hit_by_id.setdefault(h.id, h)
168
+
169
+ max_possible = KEYWORD_WEIGHT / (RRF_K + 1) + SEMANTIC_WEIGHT / (RRF_K + 1)
170
+ scored: list[tuple[SearchHit, float]] = []
171
+ for doc_id in all_ids:
172
+ rrf = 0.0
173
+ if doc_id in kw_rank:
174
+ rrf += KEYWORD_WEIGHT / (RRF_K + kw_rank[doc_id] + 1)
175
+ if doc_id in sem_rank:
176
+ rrf += SEMANTIC_WEIGHT / (RRF_K + sem_rank[doc_id] + 1)
177
+ normalized = min(rrf / max_possible, 1.0) if max_possible > 0 else 0.0
178
+ scored.append((hit_by_id[doc_id], normalized))
179
+
180
+ scored.sort(key=lambda x: x[1], reverse=True)
181
+ return scored
182
+
183
+
184
+ def _hit_to_dict(hit: SearchHit, score: float, *, global_pid: int | None = None) -> dict[str, Any]:
185
+ is_global = global_pid is not None and hit.project_id == global_pid
186
+ return {
187
+ "id": hit.id,
188
+ "project_id": None if is_global else hit.project_id,
189
+ "scope": "global" if is_global else "project",
190
+ "type": hit.source_type,
191
+ "source_id": hit.source_id,
192
+ "label": hit.label,
193
+ "path": hit.path,
194
+ "snippet": _snippet(hit.content),
195
+ "chunk_index": hit.chunk_index,
196
+ "score": round(score, 4),
197
+ }
198
+
199
+
200
+ def hybrid_search(
201
+ session: Session,
202
+ project_id: int | None,
203
+ query: str,
204
+ *,
205
+ limit: int = 25,
206
+ source_type: str | None = None,
207
+ use_semantic: bool | None = None,
208
+ ) -> list[dict[str, Any]]:
209
+ """Hybrid search with asymmetric scope visibility.
210
+
211
+ project_id given: that project's rows PLUS curated global rows (brain + library).
212
+ project_id None: global rows only — project-local knowledge stays invisible
213
+ shop-wide until promoted.
214
+ Keyword-only when pgvector/Ollama unavailable.
215
+ """
216
+ q = (query or "").strip()
217
+ if not q:
218
+ return []
219
+
220
+ global_pid = get_global_brain_shell_project_id(session)
221
+ if project_id is None:
222
+ project_ids = [global_pid]
223
+ else:
224
+ project_ids = [int(project_id)]
225
+ if global_pid not in project_ids:
226
+ project_ids.append(global_pid)
227
+
228
+ cap = max(1, min(int(limit), 50))
229
+ fetch_lim = max(cap, 50)
230
+
231
+ keyword_hits = _keyword_search(session, project_ids, q, fetch_lim, source_type=source_type)
232
+
233
+ semantic_hits: list[SearchHit] = []
234
+ pg_ok, _ = check_pgvector()
235
+ if use_semantic is not False and pg_ok:
236
+ emb_model = _get_embedding_model(session)
237
+ query_emb = generate_single_embedding(q, ollama_base_url(), emb_model)
238
+ if query_emb:
239
+ semantic_hits = _semantic_search(
240
+ session, project_ids, query_emb, fetch_lim, source_type=source_type
241
+ )
242
+
243
+ if not semantic_hits:
244
+ return [
245
+ _hit_to_dict(h, 1.0 - (i * 0.01), global_pid=global_pid)
246
+ for i, h in enumerate(keyword_hits[:cap])
247
+ ]
248
+
249
+ fused = _fuse_rrf(keyword_hits, semantic_hits)
250
+ return [_hit_to_dict(hit, score, global_pid=global_pid) for hit, score in fused[:cap]]
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ """Package status for fieldbrain_status tool."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from _db_lib import connection_summary, package_root
10
+ from _json_out import emit
11
+
12
+
13
+ def main() -> None:
14
+ parser = argparse.ArgumentParser()
15
+ args = parser.parse_args()
16
+ _ = args
17
+
18
+ root = package_root()
19
+ summary = connection_summary()
20
+
21
+ emit(
22
+ {
23
+ "ok": True,
24
+ "package_root": str(root),
25
+ "shared_dir": str(root / "shared"),
26
+ "scripts_dir": str(root / "scripts"),
27
+ "operator_chat": (
28
+ "FieldBrain package is enabled (LogicScout diagnostics included). "
29
+ "Run fieldbrain_db_check to verify Postgres + pgvector + schema. "
30
+ "Shared data lives in Postgres; Sylo private chats stay in SQLite. "
31
+ "PLC L5X parse uses sylo-logicforge — LogicScout tools explore/index only."
32
+ ),
33
+ **summary,
34
+ }
35
+ )
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,7 @@
1
+ # LogicScout shared data
2
+
3
+ **Canonical storage:** Postgres (schema v3+). Brain markdown and document file bytes live in the database so every Sylo install sees the same data.
4
+
5
+ Optional local cache under `shared/storage/` is not authoritative.
6
+
7
+ Config: `%USERPROFILE%\.sylo\fieldbrain\database_config.json` (synced from Sylo FieldBrain Settings).
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: fieldbrain
3
+ description: FieldBrain — shared shop knowledge (projects, document/brain libraries, hybrid search). Field notes and fault history live in brains. Requires sylo-fieldbrain.
4
+ metadata:
5
+ sylo:
6
+ category: domain
7
+ icon: brain
8
+ routes:
9
+ - id: fieldbrain
10
+ title: FieldBrain
11
+ icon: brain
12
+ nav_section: domain
13
+ entry: routes/fieldbrain/index.html
14
+ fallback: routes/fieldbrain/fallback.md
15
+ route_protocol_version: 0
16
+ ---
17
+
18
+ # FieldBrain
19
+
20
+ Shared shop knowledge inside Sylo: projects, document library, brain markdown, hybrid search. Field notes, faults, and fixes are **brain entries** (no separate log system). Works for PLC shops, manufacturing, or any domain where the team needs durable field notes in Postgres.
21
+
22
+ **Dashboard:** React + TypeScript route (`ui/` → Vite build), same pattern as Health and Think Tank.
23
+
24
+ **Tracker:** `features_tracker/active/2026-07-04_14-56-00_sylo_fieldbrain_package_migration.md`
25
+
26
+ ## Prerequisites
27
+
28
+ 1. **sylo-fieldbrain** optional package enabled (Capability manager).
29
+ 2. **PostgreSQL** reachable (local server or LAN address in settings / `~/.sylo/fieldbrain/database_config.json`).
30
+ 3. **pgvector** on that Postgres server (semantic search; guided setup if missing).
31
+ 4. **Ollama** for embeddings (default `http://127.0.0.1:11434`).
32
+
33
+ ## First run
34
+
35
+ 1. `fieldbrain_status` — confirm package + config paths.
36
+ 2. `fieldbrain_db_migrate` — only if auto-migrate on startup did not run (offline DB, etc.).
37
+ 3. `fieldbrain_db_check` — verify connection, pgvector, schema version on every install.
38
+
39
+ ## Document library — catalog flow (primary)
40
+
41
+ **Do not** run Marker/OCR or `fieldbrain_document_ingest` for normal library work. Search indexes a **catalog summary**, not every page of every PDF.
42
+
43
+ ### Flow
44
+
45
+ 1. **Attach** — `fieldbrain_document_attach` or `fieldbrain_document_catalog` with `file_path` registers bytes in Postgres (global shop library or project-local).
46
+ 2. **Read** — use the matching Sylo skill on the same path (operator attachment or cached copy under `~/.sylo/fieldbrain/storage/documents/`).
47
+ 3. **Catalog** — `fieldbrain_document_catalog` with your summary, category, tags, optional outline (TOC / sheet tabs / email thread subjects).
48
+ 4. **Search** — `fieldbrain_search` finds the doc by summary; open the source file with the reader when you need detail.
49
+
50
+ ### Supported file types (library)
51
+
52
+ | Extension | Read with | Typical category |
53
+ |-----------|-----------|------------------|
54
+ | `.pdf` | **sylo-pdf-reader** (`search_schematic_pdf`, region tools) | manual, datasheet, schematic |
55
+ | `.xlsx`, `.xlsm`, `.ods` | **sylo-spreadsheet** (`read_spreadsheet`) | spreadsheet, requirements |
56
+ | `.csv` | Pi **`read`** | spreadsheet, requirements |
57
+ | `.txt`, `.md` | Pi **`read`** | howto, guide, email (exported), markdown |
58
+ | `.docx` | **sylo-docx** (`read_docx`; `extract_docx_images` for embedded pictures; `render_docx` to create) | manual, requirements |
59
+ | `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.bmp` | Vision on attachment — describe panels, labels, wiring | image |
60
+
61
+ Email chains: save as `.txt` / `.md` / `.eml` export, attach, read, catalog. Raw `.msg` not supported yet.
62
+
63
+ ### Catalog tool fields
64
+
65
+ - **`description`** (required) — what it is, equipment, when to open it, key fault codes or part numbers if relevant.
66
+ - **`category`** — `manual`, `datasheet`, `requirements`, `email`, `howto`, `guide`, `schematic`, `spreadsheet`, `image`, `markdown`, `reference`, `other`.
67
+ - **`tags`** — comma-separated or JSON array (vendor, product line, machine name).
68
+ - **`outline_json`** — optional TOC or section list: `[{"level":1,"title":"Safety","page":12}]` (`page` is 0-based for PDFs).
69
+ - **`manufacturer`**, **`model`**, **`version`** — when known.
70
+
71
+ ### Images (library + brains)
72
+
73
+ - Store photos/diagrams in the **global library** (`scope=global`) with `category=image` and a vision-written `description`.
74
+ - In **brain** markdown, reference the file path or note `document #id` in prose; embed `![caption](path)` when the image lives on disk the operator can open.
75
+ - Do not duplicate heavy binaries inside brain bodies; attach once, catalog once, link in brains.
76
+
77
+ ## Tools
78
+
79
+ | Tool | When |
80
+ |------|------|
81
+ | `fieldbrain_status` | Start — paths and config |
82
+ | `fieldbrain_db_bootstrap` | One-time: create role + database (superuser), pgvector, migrate |
83
+ | `fieldbrain_db_migrate` | Apply Alembic migrations |
84
+ | `fieldbrain_db_check` | After migrate; repeat when connection fails |
85
+ | `fieldbrain_project_list` | List shared projects |
86
+ | `fieldbrain_project_create` | Create a new project (+ brain scaffold) |
87
+ | `fieldbrain_log_*` | **Legacy — do not use.** Field notes are brain entries now (see below). Read old rows with `fieldbrain_log_search` only if asked about pre-migration history. |
88
+ | `fieldbrain_document_list` | List global library or project documents |
89
+ | `fieldbrain_document_attach` | Register file bytes only (no search index) |
90
+ | `fieldbrain_document_catalog` | **Primary** — summary + tags + outline → search index |
91
+ | `fieldbrain_document_promote` | Promote project-local doc to global library (confirm with operator) |
92
+ | `fieldbrain_document_ingest` | Legacy full PDF/text chunk ingest (avoid for new docs) |
93
+ | `fieldbrain_brain_read` | Read brain markdown |
94
+ | `fieldbrain_brain_write` | Write or append brain markdown |
95
+ | `fieldbrain_brain_delete` | Soft-delete a brain doc |
96
+ | `fieldbrain_brain_restore` | Restore a soft-deleted brain doc |
97
+ | `fieldbrain_brain_revisions` | List brain doc revisions |
98
+ | `fieldbrain_search` | Hybrid keyword + semantic search |
99
+
100
+ ## Field notes are brain entries (no separate log system)
101
+
102
+ Multiple people run their own Sylo against this shared database (controls engineers, electricians). Detail level varies; **you are the normalization layer**. Whether the operator gives a full writeup or one sentence, produce a consistently structured entry.
103
+
104
+ ### Troubleshooting flow (search first)
105
+
106
+ 1. **Problem reported → search before troubleshooting.** `fieldbrain_search` with the machine's `project_id` (global knowledge is included automatically). Someone may have already fixed this.
107
+ 2. **Hit found:** surface it — file path, what fixed it last time.
108
+ 3. **No hit:** troubleshoot with the operator (LogicForge for PLC logic if enabled; docs, observations).
109
+ 4. **Resolved → write the after-action report** to the project brain. Offer this proactively when the operator says it's fixed; don't wait to be asked.
110
+
111
+ ### After-action report (AAR)
112
+
113
+ New incident: `issues/YYYY-MM-DD_<slug>.md` via `fieldbrain_brain_write`. Header first, then whatever detail the operator gave:
114
+
115
+ ```markdown
116
+ # <one-line symptom>
117
+ - date: 2026-07-04
118
+ - logged_by: <name — ask once if unknown>
119
+ - equipment: <machine/asset, if known>
120
+ - fault_code: <code(s), or "none — sequence stall / no fault">
121
+ - status: resolved | open
122
+
123
+ ## What happened
124
+ ## What fixed it
125
+ ```
126
+
127
+ Rules:
128
+
129
+ - **Fault code is optional.** Many incidents are "machine not doing what it's supposed to" with no fault, or only a generic sequence alarm. Never invent a code; write the symptom instead.
130
+ - **Terse input is fine.** "Fault X, machine stops, sensor came loose" becomes a short but complete AAR. Do not pad or ask more than one clarifying question.
131
+ - **Same issue again:** append to the existing file (`mode=append`) instead of creating a duplicate — search `issues/` first.
132
+ - **Quick capture, no resolution yet:** `inbox/<slug>.md`; distill later.
133
+ - **Recurring pattern:** distill into `gotchas/` (project) or global brain if reusable across machines.
134
+ - **Procedures:** `runbooks/`.
135
+
136
+ One file per incident keeps concurrent writers from colliding. Brain docs are versioned and soft-deleted like logs were, and `fieldbrain_search` indexes them; the consistent header keeps fault codes and equipment findable regardless of who logged it.
137
+
138
+ ## Scopes and search visibility (asymmetric)
139
+
140
+ Documents and brains have two scopes: **project** and **global**. Search visibility is one-way:
141
+
142
+ - **`fieldbrain_search` with `project_id`** — searches that project **plus** the global library/brain in one call. Do not run a second global search.
143
+ - **`fieldbrain_search` without `project_id`** — global only. Project-local knowledge stays invisible shop-wide until promoted. This is deliberate: one machine's fix can be wrong on another machine.
144
+ - Each hit carries `scope: global | project`. When citing a project hit outside its project, say so.
145
+
146
+ ### What goes where
147
+
148
+ | Content | Scope |
149
+ |---------|-------|
150
+ | Machine/commissioning issues, machine-specific gotchas, production faults | **Project brain** (that machine's project or sub-project) |
151
+ | How-tos, general process steps, anything reusable across machines | **Global brain** |
152
+ | Docs for one job only | Project library |
153
+ | Manuals, datasheets, vendor docs useful shop-wide | Global library (or promote later) |
154
+
155
+ ### Promotion
156
+
157
+ - **Documents:** `fieldbrain_document_promote` re-scopes a project-local doc to global (index rows move, embeddings kept, project link stays). Confirm with the operator first.
158
+ - **Brains:** no mechanical promote. Project brain notes are context-specific; to make one global, **rewrite it generically** and `fieldbrain_brain_write` to `scope=global`. Ask the operator before doing this.
159
+
160
+ ## Project context before any write
161
+
162
+ There is **no** default or “active” project in Sylo. The dashboard lists projects and shows `#id`; it does not pick one for chat.
163
+
164
+ Before **any write** to FieldBrain (brain entry, project-scoped document attach/catalog, project-scoped search target):
165
+
166
+ 1. Run **`fieldbrain_project_list --with-stats`** if you do not already know the ids.
167
+ 2. If the operator named a job (`12345`) or sub-project (`12345-001`), match it to a row and use that **`project_id`**.
168
+ 3. If unclear which level (job vs sub-project) or which id, **ask once** before writing: “Log this under project **12345** (#12) or sub-project **12345-001** (#15)?”
169
+ 4. Do **not** guess, do **not** create a project silently, do **not** assume the last project mentioned in an old turn.
170
+
171
+ Global library docs (`scope=global`) do not need a project. Everything with `project_id` or `scope=project` does.
172
+
173
+ ## Data split
174
+
175
+ | Data | Storage |
176
+ |------|---------|
177
+ | Chats, health, operator prefs | Sylo SQLite (private) |
178
+ | Projects, docs, brains | Postgres (shared; content in DB, soft delete + revisions) |
179
+
180
+ ## UI
181
+
182
+ Dashboard source: `packages/sylo-fieldbrain/ui/` (TypeScript + React, Vite). Built artifacts land in `skills/fieldbrain/routes/fieldbrain/` via `npm run build:ui -w sylo-fieldbrain` (included in `start-sylo.cmd`).
183
+
184
+ Open **FieldBrain** (Dashboards) for **Projects** (create/list with `#id` and stats), documents, brains, and **Settings** (Postgres + Ollama).
185
+
186
+ ## Projects
187
+
188
+ Two levels, both are separate rows in Postgres:
189
+
190
+ | Level | Example | Use for |
191
+ |-------|---------|---------|
192
+ | **Project** | `12345` | Job-wide notes, shared docs, program-level context |
193
+ | **Sub-project** | `12345-001` | Machine / line / unit-specific brains and docs |
194
+
195
+ Create in the **Projects** tab: project number `12345`, optional sub-project `001`. Or chat: `fieldbrain_project_create` with name `12345` or `12345-001`. Duplicate names rejected. List before creating. Use the `#id` shown in chat; confirm with the operator before logging.
196
+
197
+ **PLC logic** (L5X export, parse, ladder edits) lives in the **logicforge** skill (`sylo-logicforge` package), not FieldBrain.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: logicscout
3
+ description: LogicScout — shared PLC project knowledge, document/brain libraries, maintenance logs, AI diagnostics. Requires sylo-logicscout (+ sylo-logicforge for L5X/.acd tooling).
4
+ metadata:
5
+ sylo:
6
+ category: plc
7
+ icon: search
8
+ routes:
9
+ - id: logicscout
10
+ title: LogicScout
11
+ icon: search
12
+ nav_section: domain
13
+ entry: routes/logicscout/index.html
14
+ fallback: routes/logicscout/fallback.md
15
+ route_protocol_version: 0
16
+ ---
17
+
18
+ # LogicScout
19
+
20
+ Shared shop knowledge and PLC diagnostics inside Sylo. **Postgres** holds projects, document library, brain libraries, and maintenance logs. **SQLite** (Sylo host) keeps private chats.
21
+
22
+ **Tracker:** `features_tracker/active/2026-07-04_14-56-00_sylo_logicscout_package_migration.md`
23
+
24
+ ## Prerequisites
25
+
26
+ 1. **sylo-logicscout** optional package enabled (Capability manager).
27
+ 2. **PostgreSQL** reachable (local server or LAN address in settings / `~/.sylo/logicscout/database_config.json`).
28
+ 3. **pgvector** on that Postgres server (semantic search; guided setup if missing).
29
+ 4. **Ollama** for embeddings and diagnostics (default `http://127.0.0.1:11434`).
30
+ 5. **sylo-logicforge** for `.acd` → L5X and parse — do not duplicate those tools here.
31
+
32
+ ## First run
33
+
34
+ 1. `logicscout_status` — confirm package + config paths.
35
+ 2. `logicscout_db_migrate` — once on the shared database (usually the server machine).
36
+ 3. `logicscout_db_check` — verify connection, pgvector, schema version on every install.
37
+
38
+ ## Tools
39
+
40
+ | Tool | When |
41
+ |------|------|
42
+ | `logicscout_status` | Start — paths and config |
43
+ | `logicscout_db_migrate` | First-time DB setup or after package upgrade |
44
+ | `logicscout_db_check` | After migrate; repeat when connection fails |
45
+ | `logicscout_project_list` | List shared PLC projects |
46
+ | `logicscout_project_create` | Create a new project (+ brain folder scaffold) |
47
+ | `logicscout_log_create` | Record a maintenance log entry |
48
+ | `logicscout_log_update` | Update a log entry (previous version saved to revisions) |
49
+ | `logicscout_log_delete` | Soft-delete a log entry (restorable) |
50
+ | `logicscout_log_restore` | Restore a soft-deleted log entry |
51
+ | `logicscout_log_revisions` | List prior versions of a log entry |
52
+ | `logicscout_log_search` | Find past maintenance entries by fault, tag, or text |
53
+ | `logicscout_document_list` | List global library or project-attached documents |
54
+ | `logicscout_document_attach` | Attach existing doc or register a new file (bytes stored in Postgres) |
55
+ | `logicscout_document_ingest` | Ingest PDF/text: extract, chunk, index, embed (pymupdf + Ollama) |
56
+ | `logicscout_brain_read` | Read markdown from project or global brain (Postgres canonical) |
57
+ | `logicscout_brain_write` | Write or append brain markdown (revisions on replace) |
58
+ | `logicscout_brain_delete` | Soft-delete a brain doc (restorable) |
59
+ | `logicscout_brain_restore` | Restore a soft-deleted brain doc |
60
+ | `logicscout_brain_revisions` | List prior versions of a brain doc |
61
+ | `logicscout_search` | Hybrid search (keyword + pgvector when available) |
62
+ | `logicscout_l5x_explore` | Parse an L5X via LogicForge; program/routine summary + rung snippets |
63
+ | `logicscout_find_logic` | Diagnostic search over indexed L5X (optional brain/doc hits) |
64
+
65
+ ## Diagnostics
66
+
67
+ 1. **L5X on disk:** `logicscout_l5x_explore` with `l5x_path` (and optional `filter` for program/routine/tag substring). Uses **LogicForge** `parse_l5x.py` — never duplicate the parser here.
68
+ 2. **Indexed project knowledge:** `logicscout_find_logic` with `project_id` + symptom/query. Searches `search_index` for rung, routine, program, tag hits; pass `include_docs: true` for brain/document context.
69
+ 3. **Pair with LogicForge** for `.acd` export and full parse pipelines (`logicforge_*` tools).
70
+ 4. **Maintenance context:** `logicscout_log_search` before/after diagnostics to surface past faults on the same equipment.
71
+
72
+ ## Data split
73
+
74
+ | Data | Storage |
75
+ |------|---------|
76
+ | Chats, health, operator prefs | Sylo SQLite (private) |
77
+ | Projects, docs, brains, maintenance logs | Postgres (shared; schema v3: content in DB, soft delete + revisions) |
78
+
79
+ ## L5X / diagnostics
80
+
81
+ Use **logicforge** tools for `.acd` export and L5X parse. LogicScout diagnostics: `logicscout_l5x_explore` (raw L5X) and `logicscout_find_logic` (indexed project). See **Diagnostics** above.
82
+
83
+ Reference implementation (frozen): `C:\Users\YetiTrix\Documents\GitHub\LogicScout`
@@ -0,0 +1 @@
1
+ :root{--color-surface: #1a1d24;--color-surface-2: #22262f;--color-text: #e8eaed;--color-text-muted: #9aa0a6;--color-border: #3c4043;--color-accent: #6b9fff;font-family:system-ui,sans-serif;color:var(--color-text);background:var(--color-surface)}*{box-sizing:border-box}body{margin:0;min-height:100vh}code{font-size:.9em}.fieldbrain-app{max-width:960px;margin:0 auto;padding:20px 16px 40px}.fieldbrain-title{margin:0 0 4px;font-size:1.35rem}.fieldbrain-sub{margin:0 0 16px;color:var(--color-text-muted);font-size:.85rem;line-height:1.45}.fieldbrain-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}.fieldbrain-tab{border:1px solid var(--color-border);background:var(--color-surface-2);color:var(--color-text);border-radius:6px;padding:6px 12px;font-size:.85rem;cursor:pointer}.fieldbrain-tab.active{border-color:color-mix(in srgb,var(--color-accent) 50%,var(--color-border));background:color-mix(in srgb,var(--color-accent) 12%,var(--color-surface-2));color:var(--color-accent);font-weight:600}.fieldbrain-panel{margin-top:.5rem}.fieldbrain-muted{color:var(--color-text-muted);font-size:.85rem;line-height:1.45}.fieldbrain-foot{margin-top:1rem;color:var(--color-text-muted);font-size:.85rem}.fieldbrain-list{display:flex;flex-direction:column;gap:8px}.fieldbrain-row{border:1px solid var(--color-border);border-radius:6px;padding:.6rem .75rem;background:var(--color-surface-2)}.fieldbrain-row h4{margin:0 0 .35rem;font-size:.95rem}.fieldbrain-row p{margin:0}.fieldbrain-project-group{margin-bottom:1.25rem}.fieldbrain-group-title{margin:0 0 .5rem;font-size:.9rem;color:var(--color-text-muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em}.fieldbrain-project-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-bottom:.35rem}.fieldbrain-project-head h4{margin:0}.fieldbrain-project-row.is-active{border-color:color-mix(in srgb,var(--color-accent) 45%,var(--color-border));background:color-mix(in srgb,var(--color-accent) 6%,var(--color-surface-2))}.fieldbrain-stat-row{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:.35rem}.fieldbrain-stat{display:inline-block;padding:.15rem .45rem;border-radius:4px;border:1px solid var(--color-border);background:var(--color-surface);font-size:.75rem;color:var(--color-text-muted)}.fieldbrain-project-row.is-sub{margin-left:1rem;border-left:3px solid color-mix(in srgb,var(--color-accent) 35%,var(--color-border))}.fieldbrain-form label{display:block;margin:.65rem 0;font-size:.85rem}.fieldbrain-input{display:block;width:100%;max-width:28rem;margin-top:.25rem;padding:.4rem .55rem;border:1px solid var(--color-border);border-radius:6px;background:var(--color-surface);color:var(--color-text);font:inherit}.fieldbrain-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:.75rem}.fieldbrain-btn{border:1px solid var(--color-border);background:var(--color-surface-2);color:var(--color-text);border-radius:6px;padding:.45rem .85rem;font-size:.85rem;cursor:pointer}.fieldbrain-btn:hover:not(:disabled){border-color:var(--color-accent)}.fieldbrain-btn:disabled{opacity:.6;cursor:not-allowed}.fieldbrain-btn-primary{border-color:color-mix(in srgb,var(--color-accent) 50%,var(--color-border));background:color-mix(in srgb,var(--color-accent) 18%,var(--color-surface-2));color:var(--color-accent);font-weight:600}.fieldbrain-card{border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface-2);padding:14px 16px;margin-bottom:12px}.fieldbrain-card-title{margin:0 0 8px;font-size:1rem}.fieldbrain-pre{margin-top:.75rem;padding:.75rem;border:1px solid var(--color-border);border-radius:6px;background:var(--color-surface-2);font-size:.8rem;white-space:pre-wrap;overflow-x:auto}.fieldbrain-status-banner{margin-bottom:1rem;padding:.65rem .85rem;border:1px solid var(--color-border);border-radius:8px;background:color-mix(in srgb,var(--color-accent) 8%,var(--color-surface-2))}.fieldbrain-status-banner p{margin:0;font-size:.9rem}.fieldbrain-link{display:inline;padding:0;border:none;background:none;color:var(--color-accent);font:inherit;text-decoration:underline;cursor:pointer}.fieldbrain-link:hover{color:color-mix(in srgb,var(--color-accent) 80%,white)}.fieldbrain-expand-trigger{display:flex;align-items:center;gap:.55rem;width:100%;margin-bottom:1rem;padding:.65rem .85rem;border-style:dashed;text-align:left;font-weight:600}.fieldbrain-expand-trigger:hover:not(:disabled){border-color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 8%,var(--color-surface-2))}.fieldbrain-expand-icon{display:inline-flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;border-radius:4px;background:color-mix(in srgb,var(--color-accent) 18%,var(--color-surface-2));color:var(--color-accent);font-size:1rem;line-height:1;font-weight:700}.fieldbrain-create-panel{margin-bottom:1rem}.fieldbrain-create-header{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin-bottom:.35rem}.fieldbrain-create-header .fieldbrain-card-title{margin:0}.fieldbrain-toolbar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:12px;margin-bottom:.75rem}.fieldbrain-search-label{flex:1 1 14rem;margin:0;font-size:.85rem}.fieldbrain-search-input{max-width:none}.fieldbrain-toolbar-hint{margin:0;flex:1 1 auto}.fieldbrain-table-wrap{overflow-x:auto;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface-2)}.fieldbrain-table{width:100%;border-collapse:collapse;font-size:.85rem}.fieldbrain-table th,.fieldbrain-table td{padding:.5rem .65rem;border-bottom:1px solid var(--color-border);text-align:left;vertical-align:middle}.fieldbrain-table th{color:var(--color-text-muted);font-weight:600;background:color-mix(in srgb,var(--color-surface) 60%,var(--color-surface-2))}.fieldbrain-table tbody tr:last-child td{border-bottom:none}.fieldbrain-table tbody tr.is-sub-row td.fieldbrain-table-name{padding-left:1.25rem}.fieldbrain-table-empty{color:var(--color-text-muted);text-align:center;padding:1rem}.fieldbrain-table-name{font-weight:500}.fieldbrain-stat-link{border:none;background:none;color:var(--color-accent);font:inherit;font-weight:600;text-decoration:underline;cursor:pointer;padding:0}.fieldbrain-stat-link:hover{color:color-mix(in srgb,var(--color-accent) 80%,white)}.fieldbrain-filter-banner{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-bottom:.75rem;padding:.55rem .75rem;border:1px solid var(--color-border);border-radius:8px;background:color-mix(in srgb,var(--color-accent) 8%,var(--color-surface-2));font-size:.9rem}