superlocalmemory 4.0.10 → 4.1.2
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/.claude-plugin/marketplace.json +12 -2
- package/CHANGELOG.md +244 -0
- package/README.md +40 -75
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +357 -18
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +24 -2
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +36 -6
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +31 -3
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +64 -3
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +210 -256
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +20 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/server/api.py +24 -5
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_health.py +87 -10
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +191 -15
- package/src/superlocalmemory/server/ui.py +20 -4
- package/src/superlocalmemory/server/unified_daemon.py +241 -7
- package/src/superlocalmemory/storage/_migration_internals.py +54 -2
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +477 -59
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +119 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +60 -36
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +35 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +9 -18
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +19 -0
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
|
@@ -62,6 +62,7 @@ _TABLES: Final[tuple[str, ...]] = (
|
|
|
62
62
|
"community_summaries",
|
|
63
63
|
"consolidated_summaries",
|
|
64
64
|
"persona_summary",
|
|
65
|
+
"projection_outbox",
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
_FTS_TABLES: Final[tuple[str, ...]] = (
|
|
@@ -157,7 +158,7 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
|
|
|
157
158
|
content TEXT NOT NULL,
|
|
158
159
|
fact_type TEXT NOT NULL DEFAULT 'semantic'
|
|
159
160
|
CHECK (fact_type IN (
|
|
160
|
-
'episodic', 'semantic', 'opinion', '
|
|
161
|
+
'episodic', 'semantic', 'opinion', 'prospective'
|
|
161
162
|
)),
|
|
162
163
|
|
|
163
164
|
-- Entities (JSON arrays)
|
|
@@ -915,6 +916,10 @@ CREATE TABLE IF NOT EXISTS persona_summary (
|
|
|
915
916
|
# Ordered DDL list (tables before FTS, respects FK order)
|
|
916
917
|
# ---------------------------------------------------------------------------
|
|
917
918
|
|
|
919
|
+
#: Imported rather than restated so the table has exactly one definition; the
|
|
920
|
+
#: module that owns the queue owns its shape.
|
|
921
|
+
from superlocalmemory.storage.projection_outbox import DDL as _PROJECTION_OUTBOX_DDL
|
|
922
|
+
|
|
918
923
|
_DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
919
924
|
_SQL_SCHEMA_VERSION,
|
|
920
925
|
_SQL_PROFILES,
|
|
@@ -950,6 +955,12 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
|
950
955
|
CONSOLIDATED_SUMMARIES_DDL,
|
|
951
956
|
# Wave Q3: persona roll-up tier (additive; safe on existing DBs)
|
|
952
957
|
_SQL_PERSONA_SUMMARY,
|
|
958
|
+
# The queue that makes a graph/vector projection write survive a crash.
|
|
959
|
+
# Created here, at every engine init, rather than only by its migration:
|
|
960
|
+
# if this table is missing, facts stop being projected and the only symptom
|
|
961
|
+
# is a memory that cannot be recalled. That invariant must not be
|
|
962
|
+
# contingent on a migration pass having succeeded.
|
|
963
|
+
_PROJECTION_OUTBOX_DDL,
|
|
953
964
|
)
|
|
954
965
|
|
|
955
966
|
|
|
@@ -961,6 +972,18 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
|
961
972
|
#: which SQLite offers no IF NOT EXISTS form of, so presence is checked first.
|
|
962
973
|
_ADDITIVE_COLUMNS: Final[tuple[tuple[str, str, str], ...]] = (
|
|
963
974
|
("atomic_facts", "quarantined", "INTEGER NOT NULL DEFAULT 0"),
|
|
975
|
+
# ``pinned`` arrives with M015, which is a DEFERRED migration -- it runs
|
|
976
|
+
# after the engine is up. But the DDL below indexes it
|
|
977
|
+
# (``idx_facts_pinned``), and that DDL runs during engine start. On a store
|
|
978
|
+
# old enough to predate M015 the index therefore raised "no such column:
|
|
979
|
+
# pinned" before anything could add it, and every start failed the same way:
|
|
980
|
+
# the deferred pass could not run because the engine could not start, and
|
|
981
|
+
# the engine could not start because the deferred pass had not run.
|
|
982
|
+
# ``slm db migrate`` did not break the loop either -- it reports Failed=0
|
|
983
|
+
# and skips deferred migrations by definition.
|
|
984
|
+
# The column has to exist before its own index either way, so it belongs
|
|
985
|
+
# here, where a store gets it at start regardless of migration state.
|
|
986
|
+
("atomic_facts", "pinned", "INTEGER NOT NULL DEFAULT 0"),
|
|
964
987
|
)
|
|
965
988
|
|
|
966
989
|
|
|
@@ -1000,6 +1023,17 @@ def create_all_tables(conn: sqlite3.Connection) -> None:
|
|
|
1000
1023
|
"""
|
|
1001
1024
|
_set_pragmas(conn)
|
|
1002
1025
|
|
|
1026
|
+
# Before the DDL, not only after it. The DDL below indexes columns that an
|
|
1027
|
+
# upgraded store may not have yet, and an index on a column that does not
|
|
1028
|
+
# exist is a hard error, not a skipped statement -- so the whole of
|
|
1029
|
+
# create_all_tables would raise and the engine would never start.
|
|
1030
|
+
#
|
|
1031
|
+
# On a fresh database this pass does nothing: the tables do not exist yet,
|
|
1032
|
+
# which the helper treats as "nothing to alter". On an upgraded one the
|
|
1033
|
+
# tables are already there and this is exactly where the columns are owed.
|
|
1034
|
+
# It runs again at the end for tables created during this call.
|
|
1035
|
+
_add_missing_columns(conn)
|
|
1036
|
+
|
|
1003
1037
|
for ddl in _DDL_ORDERED:
|
|
1004
1038
|
conn.executescript(ddl)
|
|
1005
1039
|
|
|
@@ -631,10 +631,27 @@ class WriteCoordinator:
|
|
|
631
631
|
conn.close()
|
|
632
632
|
|
|
633
633
|
def _open_connection(self) -> sqlite3.Connection:
|
|
634
|
-
|
|
634
|
+
"""The coordinator's own connection, on the same waiting policy as the rest.
|
|
635
|
+
|
|
636
|
+
It waited one second while every other writer waited ten. A caller
|
|
637
|
+
legitimately holding the write lock for longer than a second -- a large
|
|
638
|
+
batch, a store during maintenance -- made the coordinator's next write
|
|
639
|
+
raise "database is locked" and lose the item, while an ordinary write
|
|
640
|
+
issued at the same moment simply waited and succeeded.
|
|
641
|
+
|
|
642
|
+
Reproduced: a 3,010 ms hold blocked the coordinator for 1,004 ms and then
|
|
643
|
+
dropped its write. There was no reason for the two to disagree, and the
|
|
644
|
+
shorter one belonged to the path whose failure loses data rather than
|
|
645
|
+
retries.
|
|
646
|
+
"""
|
|
647
|
+
from superlocalmemory.storage.database import _BUSY_TIMEOUT_MS
|
|
648
|
+
|
|
649
|
+
conn = sqlite3.connect(
|
|
650
|
+
str(self._db_path), timeout=_BUSY_TIMEOUT_MS / 1000.0,
|
|
651
|
+
)
|
|
635
652
|
conn.row_factory = sqlite3.Row
|
|
636
653
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
637
|
-
conn.execute("PRAGMA busy_timeout=
|
|
654
|
+
conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
|
|
638
655
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
639
656
|
return conn
|
|
640
657
|
|
|
@@ -34,6 +34,31 @@ _DEFAULT_ALPHA = 1.0
|
|
|
34
34
|
_DEFAULT_BETA = 1.0
|
|
35
35
|
_DEFAULT_TRUST = 0.5
|
|
36
36
|
|
|
37
|
+
#: Identities that are the absence of an identity. A caller that does not say
|
|
38
|
+
#: who it is lands in one of these, and on a real store the ``unknown`` bucket
|
|
39
|
+
#: had accumulated 1,708 pieces of evidence and a trust of 1.000 — the highest
|
|
40
|
+
#: of any agent in the store, higher than the named agents doing the actual
|
|
41
|
+
#: work. That number is the sum of everybody's behaviour, and it was being
|
|
42
|
+
#: handed to whoever arrived next without a name.
|
|
43
|
+
#:
|
|
44
|
+
#: It matters twice over. The trust gate lets a write through at 0.3 and a
|
|
45
|
+
#: delete at 0.5, so a caller with no identity cleared both by the widest
|
|
46
|
+
#: margin available. And a fact with no trust record of its own inherits the
|
|
47
|
+
#: trust of whoever created it, so 39 facts on that store were being ranked at
|
|
48
|
+
#: the maximum promotion for having no author.
|
|
49
|
+
#:
|
|
50
|
+
#: A catch-all is not an identity. It reads as the neutral prior every time and
|
|
51
|
+
#: accumulates nothing, which leaves the gate exactly where it was for an
|
|
52
|
+
#: anonymous caller (0.5 clears both thresholds) while removing the escalation.
|
|
53
|
+
ANONYMOUS_IDENTITIES: frozenset[str] = frozenset({
|
|
54
|
+
"", "unknown", "unspecified", "anonymous", "none", "null",
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_anonymous(agent_id: str) -> bool:
|
|
59
|
+
"""Whether this identifier names nobody in particular."""
|
|
60
|
+
return (agent_id or "").strip().lower() in ANONYMOUS_IDENTITIES
|
|
61
|
+
|
|
37
62
|
# Signal strengths (how much each event shifts alpha/beta)
|
|
38
63
|
_SIGNAL_WEIGHTS: dict[str, tuple[float, float]] = {
|
|
39
64
|
# (delta_alpha, delta_beta)
|
|
@@ -68,7 +93,14 @@ class TrustScorer:
|
|
|
68
93
|
# ------------------------------------------------------------------
|
|
69
94
|
|
|
70
95
|
def get_agent_trust(self, agent_id: str, profile_id: str) -> float:
|
|
71
|
-
"""Get trust score for an agent. Returns
|
|
96
|
+
"""Get trust score for an agent. Returns the neutral prior if unknown.
|
|
97
|
+
|
|
98
|
+
An identifier that names nobody in particular always reads as the
|
|
99
|
+
prior, whatever has been written against that bucket. See
|
|
100
|
+
``ANONYMOUS_IDENTITIES``.
|
|
101
|
+
"""
|
|
102
|
+
if is_anonymous(agent_id):
|
|
103
|
+
return _DEFAULT_TRUST
|
|
72
104
|
alpha, beta = self._get_beta_params("agent", agent_id, profile_id)
|
|
73
105
|
return self._compute_trust(alpha, beta)
|
|
74
106
|
|
|
@@ -131,6 +163,16 @@ class TrustScorer:
|
|
|
131
163
|
Returns:
|
|
132
164
|
Updated trust score for the agent.
|
|
133
165
|
"""
|
|
166
|
+
if is_anonymous(agent_id):
|
|
167
|
+
# Reputation belongs to somebody. Accumulating it against the
|
|
168
|
+
# catch-all is how that bucket reached the highest trust in the
|
|
169
|
+
# store while naming no one.
|
|
170
|
+
logger.debug(
|
|
171
|
+
"trust signal ignored: %r names nobody (signal=%s)",
|
|
172
|
+
agent_id, signal_type,
|
|
173
|
+
)
|
|
174
|
+
return _DEFAULT_TRUST
|
|
175
|
+
|
|
134
176
|
weights = _SIGNAL_WEIGHTS.get(signal_type, (0.0, 0.0))
|
|
135
177
|
delta_alpha, delta_beta = weights
|
|
136
178
|
|
|
@@ -113,27 +113,19 @@
|
|
|
113
113
|
</div>
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
|
-
<!--
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
<!-- The visible navigation is the sidebar, built by od-shell.js.
|
|
117
|
+
These hidden buttons exist only so od-shell's ``activateTab`` can
|
|
118
|
+
dispatch Bootstrap's ``shown.bs.tab`` on a real element for panes
|
|
119
|
+
it does not render itself — today that is dashboard-pane alone,
|
|
120
|
+
whose data reload hangs off that event. Every button here must
|
|
121
|
+
target a pane that exists: nine did not, and clicking one did
|
|
122
|
+
nothing at all. -->
|
|
122
123
|
<ul class="nav nav-tabs d-none" id="mainTabs" role="tablist" aria-hidden="true">
|
|
123
124
|
<li class="nav-item"><button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard-pane" type="button"></button></li>
|
|
124
125
|
<li class="nav-item"><button class="nav-link" id="graph-tab" data-bs-toggle="tab" data-bs-target="#graph-pane" type="button"></button></li>
|
|
125
126
|
<li class="nav-item"><button class="nav-link" id="memories-tab" data-bs-toggle="tab" data-bs-target="#memories-pane" type="button"></button></li>
|
|
126
|
-
<li class="nav-item"><button class="nav-link" id="recall-lab-tab" data-bs-toggle="tab" data-bs-target="#recall-lab-pane" type="button"></button></li>
|
|
127
|
-
<li class="nav-item"><button class="nav-link" id="clusters-tab" data-bs-toggle="tab" data-bs-target="#clusters-pane" type="button"></button></li>
|
|
128
|
-
<li class="nav-item"><button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button"></button></li>
|
|
129
127
|
<li class="nav-item"><button class="nav-link" id="brain-tab" data-bs-toggle="tab" data-bs-target="#brain-pane" type="button" role="tab" aria-controls="brain-pane"></button></li>
|
|
130
|
-
<li class="nav-item"><button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events-pane" type="button"></button></li>
|
|
131
128
|
<li class="nav-item"><button class="nav-link" id="agents-tab" data-bs-toggle="tab" data-bs-target="#agents-pane" type="button"></button></li>
|
|
132
|
-
<li class="nav-item"><button class="nav-link" id="trust-tab" data-bs-toggle="tab" data-bs-target="#trust-pane" type="button"></button></li>
|
|
133
|
-
<li class="nav-item"><button class="nav-link" id="lifecycle-tab" data-bs-toggle="tab" data-bs-target="#lifecycle-pane" type="button"></button></li>
|
|
134
|
-
<li class="nav-item"><button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance-pane" type="button"></button></li>
|
|
135
|
-
<li class="nav-item"><button class="nav-link" id="math-health-tab" data-bs-toggle="tab" data-bs-target="#math-health-pane" type="button"></button></li>
|
|
136
|
-
<li class="nav-item"><button class="nav-link" id="ide-tab" data-bs-toggle="tab" data-bs-target="#ide-pane" type="button"></button></li>
|
|
137
129
|
<li class="nav-item"><button class="nav-link" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-pane" type="button"></button></li>
|
|
138
130
|
</ul>
|
|
139
131
|
|
|
@@ -956,7 +948,7 @@
|
|
|
956
948
|
<h5 class="mb-0"><i class="bi bi-shield-lock text-success"></i> Compliance & audit</h5>
|
|
957
949
|
<span class="badge bg-secondary" id="compliance-profile-badge">default</span>
|
|
958
950
|
</div>
|
|
959
|
-
<p class="text-muted small mb-3">Set retention policies and access controls. In Mode A,
|
|
951
|
+
<p class="text-muted small mb-3">Set retention policies and access controls. In Mode A nothing leaves your device, which is a control your compliance programme can rely on — it is not a compliance certification, and this product does not issue one.</p>
|
|
960
952
|
<div class="row g-3 mb-3">
|
|
961
953
|
<div class="col-md-4"><div class="border rounded p-2 text-center"><div class="fw-bold fs-4" id="cp-audit-count">-</div><small class="text-muted">Audit events</small></div></div>
|
|
962
954
|
<div class="col-md-4"><div class="border rounded p-2 text-center"><div class="fw-bold fs-4" id="cp-retention-count">-</div><small class="text-muted">Retention policies</small></div></div>
|
|
@@ -1084,7 +1076,7 @@
|
|
|
1084
1076
|
<div class="btn-group w-100" role="group">
|
|
1085
1077
|
<input type="radio" class="btn-check" name="settings-mode-radio" id="mode-a-radio" value="a" checked>
|
|
1086
1078
|
<label class="btn btn-outline-success" for="mode-a-radio">
|
|
1087
|
-
<strong>Mode A</strong><br><small>Zero Cloud
|
|
1079
|
+
<strong>Mode A</strong><br><small>Zero Cloud</small>
|
|
1088
1080
|
</label>
|
|
1089
1081
|
<input type="radio" class="btn-check" name="settings-mode-radio" id="mode-b-radio" value="b">
|
|
1090
1082
|
<label class="btn btn-outline-info" for="mode-b-radio">
|
|
@@ -1585,7 +1577,6 @@
|
|
|
1585
1577
|
<!-- Neural Glass shell (v3.4.21 restructured) -->
|
|
1586
1578
|
<script src="static/js/ng-health.js?v=345"></script>
|
|
1587
1579
|
<script src="static/js/ng-ingestion.js?v=345"></script>
|
|
1588
|
-
<script src="static/js/ng-entities.js?v=3410"></script>
|
|
1589
1580
|
<script src="static/js/ng-skills.js?v=3411"></script>
|
|
1590
1581
|
<script src="static/js/ng-mesh.js?v=345"></script>
|
|
1591
1582
|
<!-- OD Shell v1.0 — replaces ng-shell.js; CSP-safe single-page tab switching -->
|
|
@@ -38,7 +38,18 @@
|
|
|
38
38
|
'load-agents': () => loadAgents(),
|
|
39
39
|
'load-clusters': () => loadClusters(),
|
|
40
40
|
'load-compliance': () => loadCompliance(),
|
|
41
|
-
|
|
41
|
+
// Re-renders through the current renderer. The button used to call a
|
|
42
|
+
// function from a superseded implementation of this pane, which was
|
|
43
|
+
// removed; naming it directly would have made Refresh throw.
|
|
44
|
+
'load-entity-explorer': () => {
|
|
45
|
+
const pane = document.getElementById('entities-pane');
|
|
46
|
+
if (pane && typeof window.odRenderEntities === 'function') {
|
|
47
|
+
pane.innerHTML = '';
|
|
48
|
+
window.odRenderEntities(pane);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof window.loadEntityExplorer === 'function') window.loadEntityExplorer();
|
|
52
|
+
},
|
|
42
53
|
'load-graph': () => loadGraph(),
|
|
43
54
|
'load-health-monitor': () => loadHealthMonitor(),
|
|
44
55
|
'load-ingestion-status': () => loadIngestionStatus(),
|
|
@@ -216,12 +216,33 @@
|
|
|
216
216
|
var strip = document.getElementById('od-h-status');
|
|
217
217
|
if (!strip) return;
|
|
218
218
|
|
|
219
|
-
// Card 1: Daemon — from /health
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
// Card 1: Daemon — from /health.
|
|
220
|
+
// `status` is the literal string "ok" on every reply the daemon is alive
|
|
221
|
+
// enough to send, so reading it said "Healthy" for a daemon that had not
|
|
222
|
+
// finished starting, or whose search was not answering. `runtime_state` is
|
|
223
|
+
// the field that actually varies.
|
|
224
|
+
var runtime = healthData ? String(healthData.runtime_state || '') : '';
|
|
225
|
+
var RUNTIME_LABELS = {
|
|
226
|
+
serving_full: 'Healthy',
|
|
227
|
+
serving_degraded: 'Degraded',
|
|
228
|
+
warming: 'Starting up',
|
|
229
|
+
not_ready: 'Not ready'
|
|
230
|
+
};
|
|
231
|
+
var daemonCls = !healthData ? 'warn'
|
|
232
|
+
: (runtime === 'serving_full' ? 'ok'
|
|
233
|
+
: (runtime === 'warming' ? 'neutral' : 'warn'));
|
|
234
|
+
var daemonVal = !healthData ? 'Unreachable'
|
|
235
|
+
: (RUNTIME_LABELS[runtime] || (healthData.status === 'ok' ? 'Healthy' : 'Degraded'));
|
|
222
236
|
var daemonDetail = healthData
|
|
223
237
|
? 'port 8765 · v' + esc(String(healthData.version || '?'))
|
|
224
238
|
: 'daemon unreachable';
|
|
239
|
+
if (healthData && healthData.projection && healthData.projection.behind) {
|
|
240
|
+
// The graph copy being behind is not a daemon fault, and it does change
|
|
241
|
+
// what a search returns, so it belongs on the card the operator reads.
|
|
242
|
+
daemonDetail += ' · graph copy ' + esc(String(healthData.projection.depth || 0))
|
|
243
|
+
+ ' behind';
|
|
244
|
+
if (daemonCls === 'ok') daemonCls = 'neutral';
|
|
245
|
+
}
|
|
225
246
|
|
|
226
247
|
// Card 2: Memory DB — from /api/stats
|
|
227
248
|
var dbMb = (statsData && statsData.overview)
|
|
@@ -239,10 +260,11 @@
|
|
|
239
260
|
var mathVal = mathCls === 'ok' ? 'Active' : (mathOverall ? esc(mathOverall) : 'Unknown');
|
|
240
261
|
var mathDetail = esc(String(mathLayerCount)) + ' / 3 layers online';
|
|
241
262
|
|
|
242
|
-
// Card 4: Mesh broker
|
|
263
|
+
// Card 4: Mesh broker. There is no endpoint for this yet, so the card says
|
|
264
|
+
// so rather than implying something was measured and came back unknown.
|
|
243
265
|
var meshCls = 'neutral';
|
|
244
|
-
var meshVal = '
|
|
245
|
-
var meshDetail = '
|
|
266
|
+
var meshVal = 'Not set up';
|
|
267
|
+
var meshDetail = 'coordinating several machines is not configured here';
|
|
246
268
|
|
|
247
269
|
var CARDS = [
|
|
248
270
|
{ label: 'Daemon', cls: daemonCls, value: daemonVal, detail: daemonDetail },
|
|
@@ -458,6 +458,25 @@
|
|
|
458
458
|
meta.style.cssText = 'margin-top:10px;font-size:12px;color:var(--fg-3);line-height:1.6';
|
|
459
459
|
meta.textContent = _summaryProvenance(d);
|
|
460
460
|
out.appendChild(meta);
|
|
461
|
+
|
|
462
|
+
/* Why it came out this way, and what to do about it.
|
|
463
|
+
*
|
|
464
|
+
* The provenance line above says a summary was assembled from the
|
|
465
|
+
* user's own notes rather than written. It never said why. Someone in
|
|
466
|
+
* Mode A — which has no language model by design — saw a plainer
|
|
467
|
+
* result than they expected with no way to learn that a written one
|
|
468
|
+
* needs a model, and would reasonably read the feature as broken.
|
|
469
|
+
*/
|
|
470
|
+
var cap = d.capability;
|
|
471
|
+
if (cap && cap.message) {
|
|
472
|
+
var hint = document.createElement('div');
|
|
473
|
+
hint.style.cssText = 'margin-top:10px;font-size:12px;line-height:1.6;' +
|
|
474
|
+
'color:var(--fg-2);background:var(--card-2);border:1px solid var(--border);' +
|
|
475
|
+
'border-left:3px solid var(--accent, var(--border));' +
|
|
476
|
+
'border-radius:var(--r-md);padding:10px 12px';
|
|
477
|
+
hint.textContent = cap.message;
|
|
478
|
+
out.appendChild(hint);
|
|
479
|
+
}
|
|
461
480
|
})
|
|
462
481
|
.catch(function (e) {
|
|
463
482
|
out.textContent = 'Could not build summary: ' + (e && e.message ? e.message : 'error');
|
|
@@ -222,6 +222,71 @@
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/* ── Layout ───────────────────────────────────────────────── */
|
|
225
|
+
/* Decay preview — three curves, one per source-trust level.
|
|
226
|
+
Shows what the kappa setting actually does before it is saved: the same
|
|
227
|
+
memory strength fading at three different rates depending on where the
|
|
228
|
+
memory came from. At kappa 0 the three curves coincide, which is the
|
|
229
|
+
honest picture of trust-modulation being off. */
|
|
230
|
+
var DECAY_TRUSTS = [
|
|
231
|
+
{ trust: 0.0, color: '#e0245e', label: 'low trust' },
|
|
232
|
+
{ trust: 0.5, color: '#f59e0b', label: 'medium' },
|
|
233
|
+
{ trust: 1.0, color: '#10b981', label: 'trusted' }
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
function buildDecayPreview() {
|
|
237
|
+
var wrap = el('div', null, { display:'flex', flexDirection:'column', gap:'6px' });
|
|
238
|
+
var cvs = el('canvas', { id: P + '-forg-decay' });
|
|
239
|
+
cvs.width = 300; cvs.height = 96;
|
|
240
|
+
Object.assign(cvs.style, {
|
|
241
|
+
border:'1px solid var(--border)', borderRadius:'4px', display:'block'
|
|
242
|
+
});
|
|
243
|
+
wrap.appendChild(cvs);
|
|
244
|
+
var key = el('div', null, { display:'flex', gap:'10px' });
|
|
245
|
+
DECAY_TRUSTS.forEach(function (t) {
|
|
246
|
+
key.appendChild(el('span', { text: t.label },
|
|
247
|
+
{ fontSize:'10.5px', color: t.color, fontFamily:'var(--font-mono)' }));
|
|
248
|
+
});
|
|
249
|
+
wrap.appendChild(key);
|
|
250
|
+
return wrap;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* The decay rate is per HOUR, and the window is chosen so the three curves
|
|
254
|
+
are actually distinguishable. At a strong memory's strength (100, the
|
|
255
|
+
configured maximum) retention over sixty days is indistinguishable from
|
|
256
|
+
zero for every trust level, so a two-month preview would draw three flat
|
|
257
|
+
lines along the bottom and say nothing. Three days separates them. */
|
|
258
|
+
var DECAY_HOURS = 72;
|
|
259
|
+
var DECAY_STRENGTH = 100.0;
|
|
260
|
+
|
|
261
|
+
function retentionAt(hours, kappa, trust) {
|
|
262
|
+
// Mirrors math/ebbinghaus.trust_modulated_retention exactly:
|
|
263
|
+
// lambda_eff = (1 / S) * (1 + kappa * (1 - trust))
|
|
264
|
+
// retention = exp(-lambda_eff * hours)
|
|
265
|
+
var lambdaEff = (1.0 / DECAY_STRENGTH) * (1.0 + kappa * (1.0 - trust));
|
|
266
|
+
return Math.exp(-lambdaEff * hours);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function drawDecayCurve(kappa) {
|
|
270
|
+
var cvs = q('forg-decay');
|
|
271
|
+
if (!cvs || !cvs.getContext) return;
|
|
272
|
+
var ctx = cvs.getContext('2d');
|
|
273
|
+
var W = cvs.width, H = cvs.height, STEPS = 72;
|
|
274
|
+
ctx.clearRect(0, 0, W, H);
|
|
275
|
+
DECAY_TRUSTS.forEach(function (t) {
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
for (var i = 0; i <= STEPS; i++) {
|
|
278
|
+
var hours = (i / STEPS) * DECAY_HOURS;
|
|
279
|
+
var retention = retentionAt(hours, kappa, t.trust);
|
|
280
|
+
var x = (i / STEPS) * (W - 2) + 1;
|
|
281
|
+
var y = H - 2 - retention * (H - 4);
|
|
282
|
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
283
|
+
}
|
|
284
|
+
ctx.strokeStyle = t.color;
|
|
285
|
+
ctx.lineWidth = 1.6;
|
|
286
|
+
ctx.stroke();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
225
290
|
function makeRow(lbl, desc, key, ctrl) {
|
|
226
291
|
var d = el('div', { 'data-set':'', 'data-txt':(lbl + ' ' + key).toLowerCase() });
|
|
227
292
|
Object.assign(d.style, { display:'flex', alignItems:'center',
|
|
@@ -719,10 +784,16 @@
|
|
|
719
784
|
intInp.style.minWidth = '80px'; intInp.style.maxWidth = '100px';
|
|
720
785
|
var statsEl = el('span', { id: P + '-forg-stats', text:'Loading…' },
|
|
721
786
|
{ fontSize:'12px', color:'var(--fg-2)', fontFamily:'var(--font-mono)' });
|
|
787
|
+
var kapR = makeRange('forg-kap', '0', '5', '0.1', '2.0');
|
|
788
|
+
var decayEl = buildDecayPreview();
|
|
722
789
|
var saveBtn = makeBtn('forg-save', 'Save Forgetting', true);
|
|
723
790
|
var st = makeSt('forg');
|
|
724
791
|
var restNote = makeRestartNote('forg');
|
|
725
792
|
saveBtn.addEventListener('click', saveForgetting);
|
|
793
|
+
kapR.querySelector('input').addEventListener('input', function () {
|
|
794
|
+
drawDecayCurve(parseFloat(this.value));
|
|
795
|
+
});
|
|
796
|
+
kapR.querySelector('input').addEventListener('change', saveForgetting);
|
|
726
797
|
onSw.addEventListener('od-toggle', saveForgetting);
|
|
727
798
|
immSw.addEventListener('od-toggle', saveForgetting);
|
|
728
799
|
arcR.querySelector('input').addEventListener('change', saveForgetting);
|
|
@@ -734,6 +805,11 @@
|
|
|
734
805
|
makeRow('Archive threshold', 'Memories below this strength move to cold archive', 'forgetting.archive_threshold', arcR),
|
|
735
806
|
makeRow('Forget threshold', 'Memories below this strength are permanently deleted', 'forgetting.forget_threshold', fgtR),
|
|
736
807
|
makeRow('Run every (minutes)', 'How often the forgetting engine runs in the background', 'forgetting.scheduler_interval_minutes', intInp),
|
|
808
|
+
makeRow('Fade untrusted sources faster (\u03ba)',
|
|
809
|
+
'Memories from sources you trust less decay quicker. 0 = every memory fades at the same rate.',
|
|
810
|
+
'forgetting.trust_kappa', kapR),
|
|
811
|
+
makeRow('Decay preview', 'Retention over 3 days at three source-trust levels',
|
|
812
|
+
'', decayEl),
|
|
737
813
|
makeRow('Memory zone stats', '', 'forgetting.stats', statsEl),
|
|
738
814
|
makeRow('Actions', '', '', bRow(saveBtn, st, restNote))
|
|
739
815
|
]);
|
|
@@ -751,6 +827,15 @@
|
|
|
751
827
|
var fgt = q('forg-fgt');
|
|
752
828
|
if (fgt) { fgt.value = d.forget_threshold || 0.05; var dF = q('forg-fgt-disp'); if (dF) dF.textContent = Number(d.forget_threshold || 0.05).toFixed(2); }
|
|
753
829
|
var it = q('forg-int'); if (it) it.value = d.scheduler_interval_minutes || 30;
|
|
830
|
+
// == null, not ||: kappa 0 is a real setting (trust-modulation off) and
|
|
831
|
+
// a falsy check would silently redraw it as the 2.0 default.
|
|
832
|
+
var kap = q('forg-kap');
|
|
833
|
+
var kv = (d.trust_kappa == null) ? 2.0 : Number(d.trust_kappa);
|
|
834
|
+
if (kap) {
|
|
835
|
+
kap.value = kv;
|
|
836
|
+
var dK = q('forg-kap-disp'); if (dK) dK.textContent = kv.toFixed(2);
|
|
837
|
+
}
|
|
838
|
+
drawDecayCurve(kv);
|
|
754
839
|
}).catch(function () {});
|
|
755
840
|
fetch('/api/v3/forgetting/stats').then(function (r) { return r.ok ? r.json() : null; })
|
|
756
841
|
.then(function (d) {
|
|
@@ -773,7 +858,8 @@
|
|
|
773
858
|
core_memory_immune: q('forg-imm') ? q('forg-imm').classList.contains('on') : true,
|
|
774
859
|
archive_threshold: parseFloat(q('forg-arc') ? q('forg-arc').value : '0.20'),
|
|
775
860
|
forget_threshold: parseFloat(q('forg-fgt') ? q('forg-fgt').value : '0.05'),
|
|
776
|
-
scheduler_interval_minutes: parseInt(q('forg-int') ? q('forg-int').value : '30') || 30
|
|
861
|
+
scheduler_interval_minutes: parseInt(q('forg-int') ? q('forg-int').value : '30') || 30,
|
|
862
|
+
trust_kappa: q('forg-kap') ? parseFloat(q('forg-kap').value) : 2.0
|
|
777
863
|
}).then(function (r) { return r.json(); })
|
|
778
864
|
.then(function (d) {
|
|
779
865
|
setSt('forg', 'Saved', true);
|
|
@@ -7,8 +7,63 @@ var recallLabState = {
|
|
|
7
7
|
perPage: 10,
|
|
8
8
|
query: '',
|
|
9
9
|
synthesis: '',
|
|
10
|
+
channelStatus: {},
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
// How each channel status reads to someone looking at a thin result set.
|
|
14
|
+
// 'empty', 'disabled' and 'not_configured' are muted on purpose: the first is a
|
|
15
|
+
// valid finding and the other two are the operator's own configuration. Only a
|
|
16
|
+
// genuine degradation is coloured as a problem.
|
|
17
|
+
var CHANNEL_STATUS_STYLE = {
|
|
18
|
+
ok: {cls: 'bg-success', label: 'ok'},
|
|
19
|
+
empty: {cls: 'bg-secondary', label: 'no hits'},
|
|
20
|
+
disabled: {cls: 'bg-secondary', label: 'off'},
|
|
21
|
+
not_configured: {cls: 'bg-secondary', label: 'n/a'},
|
|
22
|
+
error: {cls: 'bg-danger', label: 'failed'},
|
|
23
|
+
timeout: {cls: 'bg-warning', label: 'timed out'},
|
|
24
|
+
no_embedding: {cls: 'bg-danger', label: 'no embedding'},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
var CHANNEL_FAULTS = ['error', 'timeout', 'no_embedding'];
|
|
28
|
+
|
|
29
|
+
// Built with createElement and textContent throughout — the status strings come
|
|
30
|
+
// from the server, and this pane has no reason to be an injection sink.
|
|
31
|
+
function buildChannelHealth(statuses) {
|
|
32
|
+
var names = Object.keys(statuses || {});
|
|
33
|
+
if (names.length === 0) return null;
|
|
34
|
+
names.sort();
|
|
35
|
+
|
|
36
|
+
var wrap = document.createElement('div');
|
|
37
|
+
wrap.className = 'mt-2 d-flex flex-wrap align-items-center gap-1';
|
|
38
|
+
|
|
39
|
+
var degraded = names.filter(function(n) {
|
|
40
|
+
return CHANNEL_FAULTS.indexOf(statuses[n]) !== -1;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
var lead = document.createElement('span');
|
|
44
|
+
lead.className = degraded.length ? 'me-1 text-danger fw-semibold' : 'me-1 text-muted';
|
|
45
|
+
lead.style.fontSize = '0.75rem';
|
|
46
|
+
// Said plainly, because the reason this strip exists is that a partly-broken
|
|
47
|
+
// retrieval path and a store with nothing to say produced the same answer.
|
|
48
|
+
lead.textContent = degraded.length
|
|
49
|
+
? 'Retrieval degraded — this answer is missing what ' +
|
|
50
|
+
degraded.length + ' channel' + (degraded.length === 1 ? '' : 's') + ' would have found:'
|
|
51
|
+
: 'Channels:';
|
|
52
|
+
wrap.appendChild(lead);
|
|
53
|
+
|
|
54
|
+
names.forEach(function(name) {
|
|
55
|
+
var status = statuses[name];
|
|
56
|
+
var style = CHANNEL_STATUS_STYLE[status] || {cls: 'bg-dark', label: String(status)};
|
|
57
|
+
var badge = document.createElement('span');
|
|
58
|
+
badge.className = 'badge ' + style.cls;
|
|
59
|
+
badge.style.fontSize = '0.68rem';
|
|
60
|
+
badge.textContent = name + ' \u00b7 ' + style.label;
|
|
61
|
+
wrap.appendChild(badge);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return wrap;
|
|
65
|
+
}
|
|
66
|
+
|
|
12
67
|
// Delegation handles the case where the recall-lab tab pane is injected after parse time.
|
|
13
68
|
document.addEventListener('click', function(e) {
|
|
14
69
|
if (!e.target || e.target.id !== 'recall-lab-search') return;
|
|
@@ -54,6 +109,14 @@ document.addEventListener('click', function(e) {
|
|
|
54
109
|
|
|
55
110
|
recallLabState.allResults = data.results || [];
|
|
56
111
|
recallLabState.synthesis = data.synthesis || '';
|
|
112
|
+
recallLabState.channelStatus = data.channel_status || {};
|
|
113
|
+
|
|
114
|
+
// Rendered BEFORE the no-results branch below, and that ordering is the
|
|
115
|
+
// whole point: every channel failing produces exactly zero results, so
|
|
116
|
+
// the case most in need of an explanation is the one that would
|
|
117
|
+
// otherwise show a bare 'No results found'.
|
|
118
|
+
var health = buildChannelHealth(recallLabState.channelStatus);
|
|
119
|
+
if (health) metaDiv.appendChild(health);
|
|
57
120
|
|
|
58
121
|
if (recallLabState.allResults.length === 0) {
|
|
59
122
|
resultsDiv.textContent = '';
|
|
@@ -118,9 +181,21 @@ function renderRecallPage() {
|
|
|
118
181
|
numLabel.textContent = '#' + (globalIndex + 1);
|
|
119
182
|
scoreRow.appendChild(numLabel);
|
|
120
183
|
var scoreBadges = document.createElement('div');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
184
|
+
// Built element by element rather than as an HTML string. The values are
|
|
185
|
+
// server-supplied, and this file's sibling panes are held to the same
|
|
186
|
+
// rule: this pane renders DOM, it does not parse markup.
|
|
187
|
+
[
|
|
188
|
+
['badge bg-primary me-1', 'Score: ' + r.score, ''],
|
|
189
|
+
['badge bg-secondary me-1', 'Trust: ' + r.trust_score, ''],
|
|
190
|
+
['badge bg-outline-info', 'Conf: ' + r.confidence,
|
|
191
|
+
'border:1px solid #0dcaf0;color:#0dcaf0;'],
|
|
192
|
+
].forEach(function(spec) {
|
|
193
|
+
var badge = document.createElement('span');
|
|
194
|
+
badge.className = spec[0];
|
|
195
|
+
badge.textContent = spec[1];
|
|
196
|
+
if (spec[2]) badge.style.cssText = spec[2];
|
|
197
|
+
scoreBadges.appendChild(badge);
|
|
198
|
+
});
|
|
124
199
|
scoreRow.appendChild(scoreBadges);
|
|
125
200
|
item.appendChild(scoreRow);
|
|
126
201
|
|