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,429 @@
1
+ """Core LogicScout shared tables (schema version 2).
2
+
3
+ Revision ID: 002_core_tables
4
+ Revises: 001_baseline
5
+ Create Date: 2026-07-04
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+ from sqlalchemy import text
12
+
13
+ revision = "002_core_tables"
14
+ down_revision = "001_baseline"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def _pgvector_available(connection) -> bool:
20
+ row = connection.execute(
21
+ text("SELECT 1 FROM pg_extension WHERE extname = 'vector'")
22
+ ).fetchone()
23
+ return row is not None
24
+
25
+
26
+ def _try_create_pgvector(connection) -> bool:
27
+ if _pgvector_available(connection):
28
+ return True
29
+ try:
30
+ autocommit = connection.execution_options(isolation_level="AUTOCOMMIT")
31
+ autocommit.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
32
+ except Exception:
33
+ return False
34
+ return _pgvector_available(connection)
35
+
36
+
37
+ def _add_column_if_not_exists(table: str, column: str, coltype: str) -> None:
38
+ op.execute(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {column} {coltype}")
39
+
40
+
41
+ def _ensure_project_columns() -> None:
42
+ columns = [
43
+ ("max_alert_chats", "INTEGER NOT NULL DEFAULT 10"),
44
+ ("hmi_embed_token", "VARCHAR(255)"),
45
+ ("cross_project_share", "VARCHAR(20) NOT NULL DEFAULT 'share'"),
46
+ ("cross_project_search", "BOOLEAN NOT NULL DEFAULT TRUE"),
47
+ ("org_id", "VARCHAR(100) NOT NULL DEFAULT 'default'"),
48
+ ("project_tags_json", "TEXT"),
49
+ ("domain", "VARCHAR(100)"),
50
+ ("controller_family", "VARCHAR(100)"),
51
+ ("reminders_muted", "BOOLEAN NOT NULL DEFAULT FALSE"),
52
+ ("is_system_hidden", "BOOLEAN NOT NULL DEFAULT FALSE"),
53
+ ("l5x_status", "VARCHAR(20) NOT NULL DEFAULT 'analyzed'"),
54
+ ]
55
+ for name, typedef in columns:
56
+ _add_column_if_not_exists("projects", name, typedef)
57
+
58
+
59
+ def upgrade() -> None:
60
+ connection = op.get_bind()
61
+ has_pgvector = _try_create_pgvector(connection)
62
+
63
+ op.execute(
64
+ """
65
+ CREATE TABLE IF NOT EXISTS projects (
66
+ id SERIAL PRIMARY KEY,
67
+ name VARCHAR(255) NOT NULL,
68
+ controller_name VARCHAR(255),
69
+ processor_type VARCHAR(100),
70
+ software_revision VARCHAR(50),
71
+ l5x_file_path VARCHAR(500),
72
+ l5x_file_hash VARCHAR(64),
73
+ plc_ip VARCHAR(45),
74
+ plc_read_enabled BOOLEAN NOT NULL DEFAULT FALSE,
75
+ max_alert_chats INTEGER NOT NULL DEFAULT 10,
76
+ hmi_embed_token VARCHAR(255),
77
+ cross_project_share VARCHAR(20) NOT NULL DEFAULT 'share',
78
+ cross_project_search BOOLEAN NOT NULL DEFAULT TRUE,
79
+ org_id VARCHAR(100) NOT NULL DEFAULT 'default',
80
+ project_tags_json TEXT,
81
+ domain VARCHAR(100),
82
+ controller_family VARCHAR(100),
83
+ reminders_muted BOOLEAN NOT NULL DEFAULT FALSE,
84
+ is_system_hidden BOOLEAN NOT NULL DEFAULT FALSE,
85
+ l5x_status VARCHAR(20) NOT NULL DEFAULT 'analyzed',
86
+ created_at TIMESTAMP DEFAULT NOW(),
87
+ updated_at TIMESTAMP DEFAULT NOW()
88
+ )
89
+ """
90
+ )
91
+ _ensure_project_columns()
92
+ op.execute("CREATE INDEX IF NOT EXISTS ix_projects_name ON projects (name)")
93
+ op.execute("CREATE INDEX IF NOT EXISTS ix_projects_l5x_file_hash ON projects (l5x_file_hash)")
94
+ op.execute("CREATE INDEX IF NOT EXISTS ix_projects_org_id ON projects (org_id)")
95
+
96
+ op.execute(
97
+ """
98
+ CREATE TABLE IF NOT EXISTS documents (
99
+ id SERIAL PRIMARY KEY,
100
+ scope VARCHAR(20) NOT NULL DEFAULT 'project_local',
101
+ owner_project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
102
+ title VARCHAR(255) NOT NULL,
103
+ original_filename VARCHAR(255) NOT NULL,
104
+ stored_path VARCHAR(500) NOT NULL,
105
+ file_hash VARCHAR(64) NOT NULL,
106
+ mime_type VARCHAR(100),
107
+ file_size INTEGER,
108
+ category VARCHAR(20) NOT NULL DEFAULT 'other',
109
+ tags_json TEXT,
110
+ manufacturer VARCHAR(100),
111
+ model VARCHAR(100),
112
+ version VARCHAR(50),
113
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
114
+ created_at TIMESTAMP DEFAULT NOW(),
115
+ CONSTRAINT uq_documents_file_hash UNIQUE (file_hash)
116
+ )
117
+ """
118
+ )
119
+ op.execute("CREATE INDEX IF NOT EXISTS ix_documents_scope ON documents (scope)")
120
+ op.execute("CREATE INDEX IF NOT EXISTS ix_documents_owner_project_id ON documents (owner_project_id)")
121
+ op.execute("CREATE INDEX IF NOT EXISTS ix_documents_file_hash ON documents (file_hash)")
122
+ op.execute("CREATE INDEX IF NOT EXISTS ix_documents_category ON documents (category)")
123
+
124
+ op.execute(
125
+ """
126
+ CREATE TABLE IF NOT EXISTS project_document_links (
127
+ id SERIAL PRIMARY KEY,
128
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
129
+ document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
130
+ role VARCHAR(20) NOT NULL DEFAULT 'reference',
131
+ attached_at TIMESTAMP DEFAULT NOW(),
132
+ CONSTRAINT uq_project_document_link UNIQUE (project_id, document_id)
133
+ )
134
+ """
135
+ )
136
+ op.execute("CREATE INDEX IF NOT EXISTS ix_project_document_links_project_id ON project_document_links (project_id)")
137
+ op.execute("CREATE INDEX IF NOT EXISTS ix_project_document_links_document_id ON project_document_links (document_id)")
138
+ op.execute("CREATE INDEX IF NOT EXISTS ix_project_document_links_role ON project_document_links (role)")
139
+
140
+ op.execute(
141
+ """
142
+ CREATE TABLE IF NOT EXISTS global_settings (
143
+ id SERIAL PRIMARY KEY,
144
+ ollama_endpoint VARCHAR(500) DEFAULT 'http://localhost:11434',
145
+ llm_model VARCHAR(100),
146
+ summarizer_model VARCHAR(100),
147
+ vision_model VARCHAR(100),
148
+ doc_vision_model VARCHAR(100),
149
+ analysis_model VARCHAR(100),
150
+ embedding_model VARCHAR(100),
151
+ search_index_schema_version INTEGER NOT NULL DEFAULT 1,
152
+ search_query_expansion BOOLEAN NOT NULL DEFAULT TRUE,
153
+ search_reranking_enabled BOOLEAN NOT NULL DEFAULT TRUE,
154
+ search_rerank_pool INTEGER NOT NULL DEFAULT 15,
155
+ search_context_window INTEGER NOT NULL DEFAULT 1,
156
+ search_rerank_backend VARCHAR(20) NOT NULL DEFAULT 'ollama',
157
+ search_rerank_model VARCHAR(100),
158
+ ollama_num_ctx INTEGER,
159
+ ollama_num_ctx_agents_json TEXT,
160
+ ollama_profiles_json TEXT,
161
+ ollama_agent_endpoints_json TEXT,
162
+ marker_pdf_enabled BOOLEAN NOT NULL DEFAULT TRUE,
163
+ marker_pdf_use_llm BOOLEAN NOT NULL DEFAULT FALSE,
164
+ marker_pdf_ollama_model VARCHAR(100),
165
+ vision_render_dpi INTEGER NOT NULL DEFAULT 200,
166
+ max_inflight_vision INTEGER NOT NULL DEFAULT 1,
167
+ max_inflight_text INTEGER NOT NULL DEFAULT 1,
168
+ l5x_analysis_concurrency INTEGER NOT NULL DEFAULT 1,
169
+ vision_timeout_seconds INTEGER NOT NULL DEFAULT 300,
170
+ text_timeout_seconds INTEGER NOT NULL DEFAULT 180,
171
+ find_logic_llm_model VARCHAR(100),
172
+ documentation_search_llm_model VARCHAR(100),
173
+ orchestrator_temperature DOUBLE PRECISION,
174
+ summarizer_temperature DOUBLE PRECISION,
175
+ summarizer_max_summary_chars INTEGER,
176
+ vision_temperature DOUBLE PRECISION,
177
+ doc_vision_temperature DOUBLE PRECISION,
178
+ analysis_temperature DOUBLE PRECISION,
179
+ documentation_search_temperature DOUBLE PRECISION,
180
+ find_logic_temperature DOUBLE PRECISION,
181
+ search_rerank_temperature DOUBLE PRECISION,
182
+ orchestrator_max_iterations INTEGER,
183
+ find_logic_default_max_iterations INTEGER,
184
+ documentation_search_default_max_iterations INTEGER,
185
+ find_logic_effort_low_max_iterations INTEGER,
186
+ find_logic_effort_medium_max_iterations INTEGER,
187
+ find_logic_effort_high_max_iterations INTEGER,
188
+ find_logic_system_prompt_extra TEXT,
189
+ find_logic_inner_user_message TEXT,
190
+ find_logic_allowed_tools_json TEXT,
191
+ ollama_warmup_on_start BOOLEAN NOT NULL DEFAULT FALSE,
192
+ updated_at TIMESTAMP DEFAULT NOW()
193
+ )
194
+ """
195
+ )
196
+ op.execute(
197
+ """
198
+ INSERT INTO global_settings (id)
199
+ SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM global_settings WHERE id = 1)
200
+ """
201
+ )
202
+
203
+ op.execute(
204
+ """
205
+ CREATE TABLE IF NOT EXISTS document_analyses (
206
+ id SERIAL PRIMARY KEY,
207
+ document_id INTEGER NOT NULL UNIQUE REFERENCES documents(id) ON DELETE CASCADE,
208
+ llm_index_description TEXT NOT NULL,
209
+ parsed_text_with_images TEXT,
210
+ analyzed_at TIMESTAMP DEFAULT NOW()
211
+ )
212
+ """
213
+ )
214
+ op.execute("CREATE INDEX IF NOT EXISTS ix_document_analyses_document_id ON document_analyses (document_id)")
215
+
216
+ op.execute(
217
+ """
218
+ CREATE TABLE IF NOT EXISTS document_page_images (
219
+ id SERIAL PRIMARY KEY,
220
+ document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
221
+ page_number INTEGER NOT NULL,
222
+ has_images BOOLEAN NOT NULL DEFAULT TRUE,
223
+ llm_image_description TEXT,
224
+ llm_page_detail TEXT,
225
+ ocr_text TEXT,
226
+ created_at TIMESTAMP DEFAULT NOW()
227
+ )
228
+ """
229
+ )
230
+ op.execute("CREATE INDEX IF NOT EXISTS ix_document_page_images_document_id ON document_page_images (document_id)")
231
+
232
+ op.execute(
233
+ """
234
+ CREATE TABLE IF NOT EXISTS document_pdf_outline (
235
+ id SERIAL PRIMARY KEY,
236
+ document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
237
+ level INTEGER NOT NULL,
238
+ title TEXT NOT NULL,
239
+ toc_page_claimed INTEGER,
240
+ physical_pdf_page INTEGER NOT NULL,
241
+ verification VARCHAR(32) NOT NULL,
242
+ sequence INTEGER NOT NULL,
243
+ created_at TIMESTAMP DEFAULT NOW()
244
+ )
245
+ """
246
+ )
247
+ op.execute("CREATE INDEX IF NOT EXISTS ix_document_pdf_outline_document_id ON document_pdf_outline (document_id)")
248
+
249
+ op.execute(
250
+ """
251
+ CREATE TABLE IF NOT EXISTS alerts (
252
+ id SERIAL PRIMARY KEY,
253
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
254
+ tag_name VARCHAR(255) NOT NULL,
255
+ trigger_value_type VARCHAR(20) NOT NULL,
256
+ trigger_operator VARCHAR(20) NOT NULL DEFAULT 'eq',
257
+ trigger_value TEXT NOT NULL,
258
+ command TEXT NOT NULL,
259
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
260
+ created_at TIMESTAMP DEFAULT NOW(),
261
+ updated_at TIMESTAMP DEFAULT NOW()
262
+ )
263
+ """
264
+ )
265
+ op.execute("CREATE INDEX IF NOT EXISTS ix_alerts_project_id ON alerts (project_id)")
266
+
267
+ op.execute(
268
+ """
269
+ CREATE TABLE IF NOT EXISTS alert_responses (
270
+ id SERIAL PRIMARY KEY,
271
+ alert_id INTEGER NOT NULL REFERENCES alerts(id) ON DELETE CASCADE,
272
+ tag_value TEXT,
273
+ agent_response TEXT NOT NULL DEFAULT '',
274
+ is_processing BOOLEAN NOT NULL DEFAULT FALSE,
275
+ triggered_at TIMESTAMP DEFAULT NOW(),
276
+ responded_at TIMESTAMP
277
+ )
278
+ """
279
+ )
280
+ op.execute("CREATE INDEX IF NOT EXISTS ix_alert_responses_alert_id ON alert_responses (alert_id)")
281
+
282
+ op.execute(
283
+ """
284
+ CREATE TABLE IF NOT EXISTS tag_troubleshooting_solutions (
285
+ id SERIAL PRIMARY KEY,
286
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
287
+ tag_name VARCHAR(255) NOT NULL,
288
+ solution_text TEXT NOT NULL,
289
+ source_ref VARCHAR(255),
290
+ logged_by VARCHAR(100),
291
+ created_at TIMESTAMP DEFAULT NOW(),
292
+ updated_at TIMESTAMP DEFAULT NOW()
293
+ )
294
+ """
295
+ )
296
+ op.execute(
297
+ "CREATE INDEX IF NOT EXISTS ix_tag_trouble_solutions_project_tag "
298
+ "ON tag_troubleshooting_solutions (project_id, tag_name)"
299
+ )
300
+ op.execute(
301
+ "CREATE INDEX IF NOT EXISTS ix_tag_troubleshooting_solutions_tag_name "
302
+ "ON tag_troubleshooting_solutions (tag_name)"
303
+ )
304
+
305
+ op.execute(
306
+ """
307
+ CREATE TABLE IF NOT EXISTS brain_drafts (
308
+ id SERIAL PRIMARY KEY,
309
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
310
+ source_ref VARCHAR(255),
311
+ status VARCHAR(20) NOT NULL DEFAULT 'pending',
312
+ op VARCHAR(50) NOT NULL,
313
+ target_path VARCHAR(500) NOT NULL,
314
+ payload_json TEXT NOT NULL,
315
+ content_hash_before VARCHAR(64),
316
+ created_at TIMESTAMP DEFAULT NOW(),
317
+ expires_at TIMESTAMP,
318
+ applied_at TIMESTAMP
319
+ )
320
+ """
321
+ )
322
+ op.execute("CREATE INDEX IF NOT EXISTS ix_brain_drafts_project_id ON brain_drafts (project_id)")
323
+
324
+ op.execute(
325
+ """
326
+ CREATE TABLE IF NOT EXISTS maintenance_log_entries (
327
+ id SERIAL PRIMARY KEY,
328
+ project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
329
+ title VARCHAR(255) NOT NULL,
330
+ body TEXT NOT NULL,
331
+ fault_code VARCHAR(100),
332
+ equipment_tag VARCHAR(255),
333
+ logged_by VARCHAR(100),
334
+ created_at TIMESTAMP DEFAULT NOW(),
335
+ updated_at TIMESTAMP DEFAULT NOW(),
336
+ search_vector tsvector
337
+ )
338
+ """
339
+ )
340
+ op.execute("CREATE INDEX IF NOT EXISTS ix_maintenance_log_entries_project_id ON maintenance_log_entries (project_id)")
341
+ op.execute("CREATE INDEX IF NOT EXISTS ix_maintenance_log_entries_fault_code ON maintenance_log_entries (fault_code)")
342
+ op.execute(
343
+ "CREATE INDEX IF NOT EXISTS ix_maintenance_log_entries_equipment_tag ON maintenance_log_entries (equipment_tag)"
344
+ )
345
+ op.execute("CREATE INDEX IF NOT EXISTS ix_maintenance_log_entries_created_at ON maintenance_log_entries (created_at)")
346
+ op.execute(
347
+ "CREATE INDEX IF NOT EXISTS idx_maintenance_log_search_vector "
348
+ "ON maintenance_log_entries USING GIN (search_vector)"
349
+ )
350
+
351
+ op.execute(
352
+ """
353
+ CREATE OR REPLACE FUNCTION maintenance_log_entries_search_vector_update() RETURNS trigger AS $$
354
+ BEGIN
355
+ NEW.search_vector :=
356
+ setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
357
+ setweight(to_tsvector('english', coalesce(NEW.body, '')), 'B') ||
358
+ setweight(to_tsvector('english', coalesce(NEW.fault_code, '')), 'A') ||
359
+ setweight(to_tsvector('english', coalesce(NEW.equipment_tag, '')), 'A');
360
+ RETURN NEW;
361
+ END
362
+ $$ LANGUAGE plpgsql
363
+ """
364
+ )
365
+ op.execute("DROP TRIGGER IF EXISTS trg_maintenance_log_entries_search_vector ON maintenance_log_entries")
366
+ op.execute(
367
+ """
368
+ CREATE TRIGGER trg_maintenance_log_entries_search_vector
369
+ BEFORE INSERT OR UPDATE OF title, body, fault_code, equipment_tag
370
+ ON maintenance_log_entries
371
+ FOR EACH ROW EXECUTE FUNCTION maintenance_log_entries_search_vector_update()
372
+ """
373
+ )
374
+
375
+ embedding_col = ", embedding vector(768)" if has_pgvector else ""
376
+ op.execute(
377
+ f"""
378
+ CREATE TABLE IF NOT EXISTS search_index (
379
+ id SERIAL PRIMARY KEY,
380
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
381
+ source_type VARCHAR(50) NOT NULL,
382
+ source_id VARCHAR(500),
383
+ label VARCHAR(500),
384
+ path VARCHAR(500),
385
+ content TEXT,
386
+ search_vector tsvector,
387
+ chunk_index INTEGER DEFAULT 0,
388
+ section_id INTEGER NOT NULL DEFAULT 0,
389
+ entry_share VARCHAR(20) NOT NULL DEFAULT 'project_only',
390
+ brain_type VARCHAR(50),
391
+ brain_status VARCHAR(50)
392
+ {embedding_col}
393
+ )
394
+ """
395
+ )
396
+ op.execute("CREATE INDEX IF NOT EXISTS idx_search_index_vector ON search_index USING GIN (search_vector)")
397
+ op.execute("CREATE INDEX IF NOT EXISTS idx_search_index_project ON search_index (project_id)")
398
+ op.execute("CREATE INDEX IF NOT EXISTS idx_search_index_source_type ON search_index (source_type)")
399
+ if has_pgvector:
400
+ try:
401
+ op.execute(
402
+ """
403
+ CREATE INDEX IF NOT EXISTS idx_search_index_embedding
404
+ ON search_index USING hnsw (embedding vector_cosine_ops)
405
+ """
406
+ )
407
+ except Exception:
408
+ pass
409
+
410
+ op.execute("INSERT INTO logicscout_schema_meta (version) VALUES (2)")
411
+
412
+
413
+ def downgrade() -> None:
414
+ op.execute("DROP TABLE IF EXISTS search_index CASCADE")
415
+ op.execute("DROP TRIGGER IF EXISTS trg_maintenance_log_entries_search_vector ON maintenance_log_entries")
416
+ op.execute("DROP FUNCTION IF EXISTS maintenance_log_entries_search_vector_update()")
417
+ op.execute("DROP TABLE IF EXISTS maintenance_log_entries CASCADE")
418
+ op.execute("DROP TABLE IF EXISTS brain_drafts CASCADE")
419
+ op.execute("DROP TABLE IF EXISTS tag_troubleshooting_solutions CASCADE")
420
+ op.execute("DROP TABLE IF EXISTS alert_responses CASCADE")
421
+ op.execute("DROP TABLE IF EXISTS alerts CASCADE")
422
+ op.execute("DROP TABLE IF EXISTS document_pdf_outline CASCADE")
423
+ op.execute("DROP TABLE IF EXISTS document_page_images CASCADE")
424
+ op.execute("DROP TABLE IF EXISTS document_analyses CASCADE")
425
+ op.execute("DROP TABLE IF EXISTS global_settings CASCADE")
426
+ op.execute("DROP TABLE IF EXISTS project_document_links CASCADE")
427
+ op.execute("DROP TABLE IF EXISTS documents CASCADE")
428
+ op.execute("DROP TABLE IF EXISTS projects CASCADE")
429
+ op.execute("DELETE FROM logicscout_schema_meta WHERE version = 2")
@@ -0,0 +1,106 @@
1
+ """Durability: content in Postgres, soft delete, revision history (schema version 3).
2
+
3
+ Revision ID: 003_durability
4
+ Revises: 002_core_tables
5
+ Create Date: 2026-07-04
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+
12
+ revision = "003_durability"
13
+ down_revision = "002_core_tables"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ op.execute("ALTER TABLE maintenance_log_entries ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP")
20
+ op.execute(
21
+ "CREATE INDEX IF NOT EXISTS ix_maintenance_log_entries_deleted_at "
22
+ "ON maintenance_log_entries (deleted_at)"
23
+ )
24
+
25
+ op.execute(
26
+ """
27
+ CREATE TABLE IF NOT EXISTS maintenance_log_revisions (
28
+ id SERIAL PRIMARY KEY,
29
+ log_entry_id INTEGER NOT NULL REFERENCES maintenance_log_entries(id) ON DELETE CASCADE,
30
+ title VARCHAR(255) NOT NULL,
31
+ body TEXT NOT NULL,
32
+ fault_code VARCHAR(100),
33
+ equipment_tag VARCHAR(255),
34
+ logged_by VARCHAR(100),
35
+ revision_number INTEGER NOT NULL,
36
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
37
+ )
38
+ """
39
+ )
40
+ op.execute(
41
+ "CREATE INDEX IF NOT EXISTS ix_maintenance_log_revisions_entry "
42
+ "ON maintenance_log_revisions (log_entry_id, revision_number DESC)"
43
+ )
44
+
45
+ op.execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS file_content BYTEA")
46
+
47
+ op.execute(
48
+ """
49
+ CREATE TABLE IF NOT EXISTS brain_documents (
50
+ id SERIAL PRIMARY KEY,
51
+ scope VARCHAR(20) NOT NULL,
52
+ project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
53
+ relative_path VARCHAR(500) NOT NULL,
54
+ content TEXT NOT NULL DEFAULT '',
55
+ deleted_at TIMESTAMP,
56
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
57
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
58
+ )
59
+ """
60
+ )
61
+ op.execute(
62
+ "CREATE INDEX IF NOT EXISTS ix_brain_documents_scope_project "
63
+ "ON brain_documents (scope, project_id)"
64
+ )
65
+ op.execute(
66
+ """
67
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_brain_documents_global_active
68
+ ON brain_documents (relative_path)
69
+ WHERE scope = 'global' AND deleted_at IS NULL
70
+ """
71
+ )
72
+ op.execute(
73
+ """
74
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_brain_documents_project_active
75
+ ON brain_documents (project_id, relative_path)
76
+ WHERE scope = 'project' AND deleted_at IS NULL
77
+ """
78
+ )
79
+
80
+ op.execute(
81
+ """
82
+ CREATE TABLE IF NOT EXISTS brain_document_revisions (
83
+ id SERIAL PRIMARY KEY,
84
+ brain_document_id INTEGER NOT NULL REFERENCES brain_documents(id) ON DELETE CASCADE,
85
+ relative_path VARCHAR(500) NOT NULL,
86
+ content TEXT NOT NULL,
87
+ revision_number INTEGER NOT NULL,
88
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
89
+ )
90
+ """
91
+ )
92
+ op.execute(
93
+ "CREATE INDEX IF NOT EXISTS ix_brain_document_revisions_doc "
94
+ "ON brain_document_revisions (brain_document_id, revision_number DESC)"
95
+ )
96
+
97
+ op.execute("INSERT INTO logicscout_schema_meta (version) VALUES (3)")
98
+
99
+
100
+ def downgrade() -> None:
101
+ op.execute("DROP TABLE IF EXISTS brain_document_revisions CASCADE")
102
+ op.execute("DROP TABLE IF EXISTS brain_documents CASCADE")
103
+ op.execute("DROP TABLE IF EXISTS maintenance_log_revisions CASCADE")
104
+ op.execute("ALTER TABLE documents DROP COLUMN IF EXISTS file_content")
105
+ op.execute("ALTER TABLE maintenance_log_entries DROP COLUMN IF EXISTS deleted_at")
106
+ op.execute("DELETE FROM logicscout_schema_meta WHERE version = 3")
@@ -0,0 +1,42 @@
1
+ """Add search_index.embedding when pgvector is available (schema version 4).
2
+
3
+ Revision ID: 004_pgvector_embedding
4
+ Revises: 003_durability
5
+ Create Date: 2026-07-04
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+ from sqlalchemy import text
12
+
13
+ revision = "004_pgvector_embedding"
14
+ down_revision = "003_durability"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def _pgvector_available(connection) -> bool:
20
+ row = connection.execute(
21
+ text("SELECT 1 FROM pg_extension WHERE extname = 'vector'")
22
+ ).fetchone()
23
+ return row is not None
24
+
25
+
26
+ def upgrade() -> None:
27
+ connection = op.get_bind()
28
+ if _pgvector_available(connection):
29
+ op.execute("ALTER TABLE search_index ADD COLUMN IF NOT EXISTS embedding vector(768)")
30
+ op.execute(
31
+ """
32
+ CREATE INDEX IF NOT EXISTS idx_search_index_embedding
33
+ ON search_index USING hnsw (embedding vector_cosine_ops)
34
+ """
35
+ )
36
+ op.execute("INSERT INTO logicscout_schema_meta (version) VALUES (4)")
37
+
38
+
39
+ def downgrade() -> None:
40
+ op.execute("DROP INDEX IF EXISTS idx_search_index_embedding")
41
+ op.execute("ALTER TABLE search_index DROP COLUMN IF EXISTS embedding")
42
+ op.execute("DELETE FROM logicscout_schema_meta WHERE version = 4")
@@ -0,0 +1,37 @@
1
+ """Add projects.job_number for grouping sub-jobs (schema version 5).
2
+
3
+ Revision ID: 005_project_job_number
4
+ Revises: 004_pgvector_embedding
5
+ Create Date: 2026-07-04
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+
12
+ revision = "005_project_job_number"
13
+ down_revision = "004_pgvector_embedding"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ op.execute("ALTER TABLE projects ADD COLUMN IF NOT EXISTS job_number VARCHAR(20)")
20
+ op.execute(
21
+ """
22
+ UPDATE projects
23
+ SET job_number = split_part(name, '-', 1)
24
+ WHERE job_number IS NULL
25
+ AND name ~ '^[0-9]{5}-[0-9]{3}$'
26
+ """
27
+ )
28
+ op.execute(
29
+ "CREATE INDEX IF NOT EXISTS ix_projects_job_number ON projects (job_number)"
30
+ )
31
+ op.execute("INSERT INTO logicscout_schema_meta (version) VALUES (5)")
32
+
33
+
34
+ def downgrade() -> None:
35
+ op.execute("DROP INDEX IF EXISTS ix_projects_job_number")
36
+ op.execute("ALTER TABLE projects DROP COLUMN IF EXISTS job_number")
37
+ op.execute("DELETE FROM logicscout_schema_meta WHERE version = 5")
@@ -0,0 +1,62 @@
1
+ """Add projects.parent_project_id for job vs sub-project hierarchy (schema version 6).
2
+
3
+ Revision ID: 006_project_parent
4
+ Revises: 005_project_job_number
5
+ Create Date: 2026-07-04
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+
12
+ revision = "006_project_parent"
13
+ down_revision = "005_project_job_number"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ op.execute(
20
+ """
21
+ ALTER TABLE projects
22
+ ADD COLUMN IF NOT EXISTS parent_project_id INTEGER
23
+ REFERENCES projects(id) ON DELETE SET NULL
24
+ """
25
+ )
26
+ op.execute(
27
+ "CREATE INDEX IF NOT EXISTS ix_projects_parent_project_id ON projects (parent_project_id)"
28
+ )
29
+ op.execute(
30
+ """
31
+ UPDATE projects sub
32
+ SET parent_project_id = parent.id
33
+ FROM projects parent
34
+ WHERE sub.name ~ '^[0-9]{5}-[0-9]{3}$'
35
+ AND parent.name = split_part(sub.name, '-', 1)
36
+ AND parent.name ~ '^[0-9]{5}$'
37
+ AND sub.parent_project_id IS NULL
38
+ """
39
+ )
40
+ op.execute(
41
+ """
42
+ UPDATE projects
43
+ SET job_number = split_part(name, '-', 1)
44
+ WHERE job_number IS NULL
45
+ AND name ~ '^[0-9]{5}-[0-9]{3}$'
46
+ """
47
+ )
48
+ op.execute(
49
+ """
50
+ UPDATE projects
51
+ SET job_number = name
52
+ WHERE job_number IS NULL
53
+ AND name ~ '^[0-9]{5}$'
54
+ """
55
+ )
56
+ op.execute("INSERT INTO logicscout_schema_meta (version) VALUES (6)")
57
+
58
+
59
+ def downgrade() -> None:
60
+ op.execute("DROP INDEX IF EXISTS ix_projects_parent_project_id")
61
+ op.execute("ALTER TABLE projects DROP COLUMN IF EXISTS parent_project_id")
62
+ op.execute("DELETE FROM logicscout_schema_meta WHERE version = 6")
@@ -0,0 +1 @@
1
+ """Alembic revision scripts for sylo-logicscout."""