@cleocode/core 2026.4.58 → 2026.4.60

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 (32) hide show
  1. package/dist/index.js +488 -11
  2. package/dist/index.js.map +3 -3
  3. package/dist/internal.js +497 -13
  4. package/dist/internal.js.map +3 -3
  5. package/dist/memory/graph-queries.d.ts.map +1 -1
  6. package/dist/store/agent-registry-accessor.d.ts +1 -1
  7. package/dist/store/brain-accessor.d.ts +57 -1
  8. package/dist/store/brain-accessor.d.ts.map +1 -1
  9. package/dist/store/brain-schema.d.ts +738 -0
  10. package/dist/store/brain-schema.d.ts.map +1 -1
  11. package/dist/store/brain-sqlite.d.ts.map +1 -1
  12. package/dist/store/nexus-schema.d.ts +1 -1
  13. package/dist/system/health.d.ts.map +1 -1
  14. package/dist/validation/doctor/checks.d.ts +7 -0
  15. package/dist/validation/doctor/checks.d.ts.map +1 -1
  16. package/migrations/drizzle-brain/20260416000001_t673-retrieval-log-plasticity-columns/migration.sql +57 -0
  17. package/migrations/drizzle-brain/20260416000002_t673-plasticity-events-expand/migration.sql +44 -0
  18. package/migrations/drizzle-brain/20260416000003_t673-page-edges-plasticity-columns/migration.sql +44 -0
  19. package/migrations/drizzle-brain/20260416000004_t673-new-plasticity-tables/migration.sql +73 -0
  20. package/package.json +8 -8
  21. package/src/memory/__tests__/brain-retrieval-m1.test.ts +250 -0
  22. package/src/memory/__tests__/brain-schema-m2-m3.test.ts +418 -0
  23. package/src/memory/__tests__/brain-schema-m4.test.ts +494 -0
  24. package/src/memory/brain-retrieval.ts +1 -1
  25. package/src/memory/graph-queries.ts +14 -0
  26. package/src/store/agent-registry-accessor.ts +1 -1
  27. package/src/store/brain-accessor.ts +120 -0
  28. package/src/store/brain-schema.ts +373 -1
  29. package/src/store/brain-sqlite.ts +123 -0
  30. package/src/system/health.ts +4 -1
  31. package/src/validation/doctor/checks.ts +107 -0
  32. package/src/validation/protocols/protocols-markdown/research.md +1 -1
@@ -668,12 +668,83 @@ export const brainPageEdges = sqliteTable(
668
668
  provenance: text('provenance'),
669
669
 
670
670
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
671
+
672
+ // === T673-M3: Plasticity tracking columns ===
673
+
674
+ /**
675
+ * ISO 8601 timestamp of the last LTP event applied to this edge.
676
+ * Used by the decay pass: edges with (now - last_reinforced_at) > decay_threshold_days
677
+ * receive a per-day weight decay. Null = never reinforced (structural/semantic edges).
678
+ * Only populated when plasticity_class IN ('hebbian', 'stdp').
679
+ *
680
+ * @task T706
681
+ */
682
+ lastReinforcedAt: text('last_reinforced_at'),
683
+
684
+ /**
685
+ * Count of LTP (potentiation) events applied to this edge lifetime.
686
+ * Incremented on every LTP write. Used to compute stability_score.
687
+ *
688
+ * @task T706
689
+ */
690
+ reinforcementCount: integer('reinforcement_count').notNull().default(0),
691
+
692
+ /**
693
+ * Plasticity class governing which algorithm(s) write to this edge.
694
+ *
695
+ * - 'static': Non-plastic edge (structural, semantic, etc.). Immune to decay.
696
+ * - 'hebbian': Written by strengthenCoRetrievedEdges. Subject to decay.
697
+ * - 'stdp': Written or refined by applyStdpPlasticity. Subject to decay + LTD.
698
+ *
699
+ * Edges start 'static' for all non-co_retrieved types.
700
+ * co_retrieved edges start 'hebbian' (seeded by M3 migration), can upgrade to 'stdp'.
701
+ *
702
+ * @task T706
703
+ */
704
+ plasticityClass: text('plasticity_class', {
705
+ enum: ['static', 'hebbian', 'stdp'] as const,
706
+ })
707
+ .notNull()
708
+ .default('static'),
709
+
710
+ /**
711
+ * ISO 8601 timestamp of the last LTD (depression) event on this edge.
712
+ * Null = never depressed. Used for debugging and Studio viz animation.
713
+ *
714
+ * @task T706
715
+ */
716
+ lastDepressedAt: text('last_depressed_at'),
717
+
718
+ /**
719
+ * Count of LTD (depression) events applied to this edge lifetime.
720
+ * Enables analysis of edges that are persistently weakened.
721
+ *
722
+ * @task T706
723
+ */
724
+ depressionCount: integer('depression_count').notNull().default(0),
725
+
726
+ /**
727
+ * Biological-analog stability score: 0.0 (unstable) – 1.0 (consolidated).
728
+ *
729
+ * Computed by runConsolidation decay pass as:
730
+ * stability = tanh(reinforcement_count / 10) × exp(-(days_since_reinforced / 30))
731
+ *
732
+ * Null = not yet computed (new edges). Enables fast filtering in decay pass:
733
+ * edges with stability > 0.9 skip the full decay recalculation.
734
+ * Updated at session-end consolidation, NOT per-event.
735
+ *
736
+ * @task T706
737
+ */
738
+ stabilityScore: real('stability_score'),
671
739
  },
672
740
  (table) => [
673
741
  primaryKey({ columns: [table.fromId, table.toId, table.edgeType] }),
674
742
  index('idx_brain_edges_from').on(table.fromId),
675
743
  index('idx_brain_edges_to').on(table.toId),
676
744
  index('idx_brain_edges_type').on(table.edgeType),
745
+ index('idx_brain_edges_last_reinforced').on(table.lastReinforcedAt),
746
+ index('idx_brain_edges_plasticity_class').on(table.plasticityClass),
747
+ index('idx_brain_edges_stability').on(table.stabilityScore),
677
748
  ],
678
749
  );
679
750
 
@@ -687,9 +758,20 @@ export const brainPageEdges = sqliteTable(
687
758
  * - Co-retrieval edge strengthening (consolidation step 6)
688
759
  * - Memory quality instrumentation (retrieval frequency tracking)
689
760
  * - Citation count validation (corroboration for tier promotion)
761
+ * - STDP plasticity — spike-timing pairs derived from retrieval timestamps (T673)
690
762
  *
691
763
  * Each row records one retrieval event: the query, which entries were returned,
692
764
  * and the retrieval source (find/fetch/hybrid).
765
+ *
766
+ * Column notes (T673-M1):
767
+ * entry_ids — stored as JSON array string '["id1","id2"]' (never CSV).
768
+ * Writer: JSON.stringify(entryIds). Readers: JSON.parse(row.entry_ids).
769
+ * M1 migration converts existing CSV rows to JSON format.
770
+ * session_id — synced to live table via M1 ALTER (was missing from live DDL).
771
+ * reward_signal — R-STDP third-factor: +1.0 verified | +0.5 done | -0.5 cancelled | null.
772
+ * retrieval_order — existed in live table via self-healing DDL but was absent in Drizzle.
773
+ * M1 brings Drizzle into sync (schema drift fix).
774
+ * delta_ms — same schema drift resolution as retrieval_order.
693
775
  */
694
776
  export const brainRetrievalLog = sqliteTable(
695
777
  'brain_retrieval_log',
@@ -699,7 +781,12 @@ export const brainRetrievalLog = sqliteTable(
699
781
  /** The search query or fetch IDs that triggered this retrieval. */
700
782
  query: text('query').notNull(),
701
783
 
702
- /** Comma-separated list of entry IDs returned in this retrieval. */
784
+ /**
785
+ * JSON array of entry IDs returned in this retrieval.
786
+ * Stored as JSON array string: '["obs:A","obs:B"]'.
787
+ * Always write with JSON.stringify() — NEVER join(',').
788
+ * Readers call JSON.parse(). Migration M1 converts any pre-existing CSV rows.
789
+ */
703
790
  entryIds: text('entry_ids').notNull(),
704
791
 
705
792
  /** Number of entries returned. */
@@ -715,11 +802,29 @@ export const brainRetrievalLog = sqliteTable(
715
802
  sessionId: text('session_id'),
716
803
 
717
804
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
805
+
806
+ // === T673-M1: STDP plasticity columns ===
807
+
808
+ /** Sequence position of this retrieval within a batch query (0-based). */
809
+ retrievalOrder: integer('retrieval_order'),
810
+
811
+ /** Wall-clock ms since the previous retrieval row in the same batch. */
812
+ deltaMs: integer('delta_ms'),
813
+
814
+ /**
815
+ * R-STDP reward signal: scalar [-1.0, +1.0], null = unlabeled.
816
+ * Populated by backfillRewardSignals() at session end (Step 9a).
817
+ * +1.0 = task verified and passed | +0.5 = done (unverified) | -0.5 = cancelled.
818
+ * Per D-BRAIN-VIZ-13. backfillRewardSignals MUST skip rows where
819
+ * session_id LIKE 'ses_backfill_%' (synthetic historical sessions, no task correlation).
820
+ */
821
+ rewardSignal: real('reward_signal'),
718
822
  },
719
823
  (table) => [
720
824
  index('idx_retrieval_log_created').on(table.createdAt),
721
825
  index('idx_retrieval_log_source').on(table.source),
722
826
  index('idx_retrieval_log_session').on(table.sessionId),
827
+ index('idx_retrieval_log_reward').on(table.rewardSignal),
723
828
  ],
724
829
  );
725
830
 
@@ -759,6 +864,53 @@ export const brainPlasticityEvents = sqliteTable(
759
864
  timestamp: text('timestamp').notNull().default(sql`(datetime('now'))`),
760
865
  /** Session ID that triggered the STDP pass, if available. */
761
866
  sessionId: text('session_id'),
867
+
868
+ // === T673-M2: Observability columns ===
869
+
870
+ /**
871
+ * Edge weight immediately BEFORE this plasticity event was applied.
872
+ * Null on the first LTP event that inserts a new edge (edge didn't exist).
873
+ * Enables "show learning history" in Studio viz without querying brain_weight_history.
874
+ *
875
+ * @task T696
876
+ */
877
+ weightBefore: real('weight_before'),
878
+
879
+ /**
880
+ * Edge weight immediately AFTER this plasticity event was applied.
881
+ * Computed as CLAMP(weight_before + delta_w, 0.0, 1.0).
882
+ * Redundant with delta_w but enables fast before/after display without arithmetic.
883
+ *
884
+ * @task T696
885
+ */
886
+ weightAfter: real('weight_after'),
887
+
888
+ /**
889
+ * Soft FK to brain_retrieval_log.id — the retrieval row that triggered this pair.
890
+ * Null for externally-triggered or legacy events.
891
+ * Enables: "which memory retrieval caused this edge to strengthen?"
892
+ *
893
+ * @task T696
894
+ */
895
+ retrievalLogId: integer('retrieval_log_id'),
896
+
897
+ /**
898
+ * R-STDP reward signal active when this event fired.
899
+ * Copied from the retrieval_log row's reward_signal at time of plasticity pass.
900
+ * Null = unmodulated. Denormalized for fast filtering without a JOIN.
901
+ *
902
+ * @task T696
903
+ */
904
+ rewardSignal: real('reward_signal'),
905
+
906
+ /**
907
+ * Wall-clock milliseconds between the two spikes that generated this event.
908
+ * Pre-computed at INSERT time — avoids re-deriving from retrieval timestamps.
909
+ * Enables analysis of STDP window distribution.
910
+ *
911
+ * @task T696
912
+ */
913
+ deltaTMs: integer('delta_t_ms'),
762
914
  },
763
915
  (table) => [
764
916
  index('idx_plasticity_source').on(table.sourceNode),
@@ -766,6 +918,210 @@ export const brainPlasticityEvents = sqliteTable(
766
918
  index('idx_plasticity_timestamp').on(table.timestamp),
767
919
  index('idx_plasticity_session').on(table.sessionId),
768
920
  index('idx_plasticity_kind').on(table.kind),
921
+ index('idx_plasticity_retrieval_log').on(table.retrievalLogId),
922
+ index('idx_plasticity_reward').on(table.rewardSignal),
923
+ ],
924
+ );
925
+
926
+ // ============================================================================
927
+ // WEIGHT HISTORY — immutable per-edge Δw audit log (T673-M4, T697)
928
+ // ============================================================================
929
+
930
+ /**
931
+ * Immutable audit log of every edge weight change (LTP, LTD, Hebbian, prune,
932
+ * external). Routine exponential decay writes do NOT appear here — only discrete
933
+ * plasticity events that cross the 1e-6 negligibility threshold.
934
+ *
935
+ * Retention policy: rolling 90 days. runConsolidation Step 9d DELETE sweep
936
+ * purges rows older than 90 days. Actual pruning wired in Wave 3 (T690).
937
+ *
938
+ * Spec: docs/specs/stdp-wire-up-spec.md §2.1.4 (owner Q4 mandate — in scope).
939
+ *
940
+ * @task T697
941
+ * @epic T673
942
+ */
943
+ export const brainWeightHistory = sqliteTable(
944
+ 'brain_weight_history',
945
+ {
946
+ id: integer('id').primaryKey({ autoIncrement: true }),
947
+
948
+ /** from_id of the affected brain_page_edges row. */
949
+ edgeFromId: text('edge_from_id').notNull(),
950
+
951
+ /** to_id of the affected brain_page_edges row. */
952
+ edgeToId: text('edge_to_id').notNull(),
953
+
954
+ /** Edge type of the affected brain_page_edges row (e.g. 'co_retrieved'). */
955
+ edgeType: text('edge_type').notNull(),
956
+
957
+ /** Edge weight immediately before this event. Null if the edge was just created. */
958
+ weightBefore: real('weight_before'),
959
+
960
+ /** Edge weight after this event. CLAMP(weightBefore + deltaWeight, 0, 1). NOT NULL. */
961
+ weightAfter: real('weight_after').notNull(),
962
+
963
+ /**
964
+ * Signed weight delta applied to the edge.
965
+ * Positive = potentiation (LTP/Hebbian), negative = depression (LTD).
966
+ * Prune events record the final weight that triggered deletion (negative).
967
+ */
968
+ deltaWeight: real('delta_weight').notNull(),
969
+
970
+ /**
971
+ * Plasticity event kind.
972
+ * 'ltp' — Long-Term Potentiation (STDP pre-before-post)
973
+ * 'ltd' — Long-Term Depression (STDP post-before-pre)
974
+ * 'hebbian' — Co-retrieval Hebbian strengthening
975
+ * 'decay' — Temporal decay (only prune-triggering decays written here)
976
+ * 'prune' — Edge deleted (weight fell below min_weight threshold)
977
+ * 'external' — Manually-applied external weight change
978
+ */
979
+ eventKind: text('event_kind').notNull(),
980
+
981
+ /** Soft FK to brain_plasticity_events.id — the STDP event that caused this. */
982
+ sourcePlasticityEventId: integer('source_plasticity_event_id'),
983
+
984
+ /** Soft FK to brain_retrieval_log.id — the retrieval batch that triggered this. */
985
+ retrievalLogId: integer('retrieval_log_id'),
986
+
987
+ /** R-STDP reward signal at time of event (copied from retrieval_log.reward_signal). */
988
+ rewardSignal: real('reward_signal'),
989
+
990
+ /** ISO 8601 timestamp when this weight change was applied. */
991
+ changedAt: text('changed_at').notNull().default(sql`(datetime('now'))`),
992
+ },
993
+ (table) => [
994
+ index('idx_weight_history_edge').on(table.edgeFromId, table.edgeToId, table.edgeType),
995
+ index('idx_weight_history_from').on(table.edgeFromId),
996
+ index('idx_weight_history_to').on(table.edgeToId),
997
+ index('idx_weight_history_changed_at').on(table.changedAt),
998
+ index('idx_weight_history_event_kind').on(table.eventKind),
999
+ index('idx_weight_history_plasticity_event').on(table.sourcePlasticityEventId),
1000
+ ],
1001
+ );
1002
+
1003
+ // ============================================================================
1004
+ // BRAIN MODULATORS — R-STDP neuromodulator event log (T673-M4, T699)
1005
+ // ============================================================================
1006
+
1007
+ /**
1008
+ * Discrete neuromodulator event log for R-STDP third-factor gating.
1009
+ * Records every reward/correction/feedback signal that modulates plasticity.
1010
+ * Inserted by backfillRewardSignals for each task outcome it processes.
1011
+ *
1012
+ * Both writes (retrieval_log UPDATE and modulators INSERT) use two separate
1013
+ * SQLite connections — no ATTACH — matching the cross-db-cleanup.ts pattern.
1014
+ *
1015
+ * Spec: docs/specs/stdp-wire-up-spec.md §2.1.5 (Lead A §4.5).
1016
+ *
1017
+ * @task T699
1018
+ * @epic T673
1019
+ */
1020
+ export const brainModulators = sqliteTable(
1021
+ 'brain_modulators',
1022
+ {
1023
+ id: integer('id').primaryKey({ autoIncrement: true }),
1024
+
1025
+ /**
1026
+ * Modulator event type. String (not enum constraint) for extensibility.
1027
+ * Expected values: 'task_verified'|'task_completed'|'task_cancelled'|
1028
+ * 'owner_verify'|'session_success'|'session_blocker'|'external'
1029
+ */
1030
+ modulatorType: text('modulator_type').notNull(),
1031
+
1032
+ /**
1033
+ * Reward valence in range [-1.0, +1.0].
1034
+ * +1.0 = strong reward (verified correct task)
1035
+ * +0.5 = moderate reward (done, unverified)
1036
+ * -0.5 = mild correction (cancelled task)
1037
+ * -1.0 = strong correction (explicit invalidation)
1038
+ * 0.0 = neutral signal
1039
+ */
1040
+ valence: real('valence').notNull(),
1041
+
1042
+ /**
1043
+ * Magnitude 0.0–1.0 confidence scaling.
1044
+ * Effective reward = valence × magnitude.
1045
+ * Defaults to 1.0 (full confidence).
1046
+ */
1047
+ magnitude: real('magnitude').notNull().default(1.0),
1048
+
1049
+ /** Polymorphic source event ID — task ID, memory entry ID, or other string ref. */
1050
+ sourceEventId: text('source_event_id'),
1051
+
1052
+ /** Session ID (soft FK to tasks.db sessions). */
1053
+ sessionId: text('session_id'),
1054
+
1055
+ /** Human-readable description of why this modulator was emitted. */
1056
+ description: text('description'),
1057
+
1058
+ /** ISO 8601 timestamp when this modulator event was recorded. */
1059
+ createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
1060
+ },
1061
+ (table) => [
1062
+ index('idx_modulators_type').on(table.modulatorType),
1063
+ index('idx_modulators_session').on(table.sessionId),
1064
+ index('idx_modulators_created_at').on(table.createdAt),
1065
+ index('idx_modulators_source_event').on(table.sourceEventId),
1066
+ index('idx_modulators_valence').on(table.valence),
1067
+ ],
1068
+ );
1069
+
1070
+ // ============================================================================
1071
+ // BRAIN CONSOLIDATION EVENTS — pipeline run audit log (T673-M4, T701)
1072
+ // ============================================================================
1073
+
1074
+ /**
1075
+ * One row per runConsolidation execution. Enables T628 auto-dream scheduling
1076
+ * and pipeline observability. Required by the auto-dream cycle for scheduling.
1077
+ *
1078
+ * runConsolidation in brain-lifecycle.ts MUST accept an optional trigger
1079
+ * parameter and INSERT one row per run with step_results_json + duration_ms.
1080
+ *
1081
+ * Spec: docs/specs/stdp-wire-up-spec.md §2.1.6 (Lead A + Lead C joint).
1082
+ *
1083
+ * @task T701
1084
+ * @epic T673
1085
+ */
1086
+ export const brainConsolidationEvents = sqliteTable(
1087
+ 'brain_consolidation_events',
1088
+ {
1089
+ id: integer('id').primaryKey({ autoIncrement: true }),
1090
+
1091
+ /**
1092
+ * What triggered this consolidation run. String (not enum constraint) for
1093
+ * forward compatibility with T628 scheduler.
1094
+ * Expected values: 'session_end' | 'maintenance' | 'scheduled' | 'manual'
1095
+ */
1096
+ trigger: text('trigger').notNull(),
1097
+
1098
+ /** Session ID that initiated this consolidation (soft FK to tasks.db sessions). */
1099
+ sessionId: text('session_id'),
1100
+
1101
+ /**
1102
+ * JSON-serialized ConsolidationResult — all per-step counts and metrics.
1103
+ * Shape: { [stepName: string]: { count: number, durationMs?: number } }
1104
+ * Required NOT NULL — every run must record its results for T628 scheduling.
1105
+ */
1106
+ stepResultsJson: text('step_results_json').notNull(),
1107
+
1108
+ /** Wall-clock milliseconds from start to completion. Null if run did not complete. */
1109
+ durationMs: integer('duration_ms'),
1110
+
1111
+ /**
1112
+ * Whether the run succeeded.
1113
+ * Stored as integer(boolean) per Drizzle SQLite boolean convention.
1114
+ * true = completed without unhandled error, false = partial or error.
1115
+ */
1116
+ succeeded: integer('succeeded', { mode: 'boolean' }).notNull().default(true),
1117
+
1118
+ /** ISO 8601 timestamp when this consolidation run started. */
1119
+ startedAt: text('started_at').notNull().default(sql`(datetime('now'))`),
1120
+ },
1121
+ (table) => [
1122
+ index('idx_consolidation_events_started_at').on(table.startedAt),
1123
+ index('idx_consolidation_events_trigger').on(table.trigger),
1124
+ index('idx_consolidation_events_session').on(table.sessionId),
769
1125
  ],
770
1126
  );
771
1127
 
@@ -791,4 +1147,20 @@ export type BrainStickyNoteRow = typeof brainStickyNotes.$inferSelect;
791
1147
  export type NewBrainStickyNoteRow = typeof brainStickyNotes.$inferInsert;
792
1148
  export type BrainPlasticityEventRow = typeof brainPlasticityEvents.$inferSelect;
793
1149
  export type NewBrainPlasticityEventRow = typeof brainPlasticityEvents.$inferInsert;
1150
+
1151
+ /** Row type for brain_weight_history SELECT queries. */
1152
+ export type BrainWeightHistoryRow = typeof brainWeightHistory.$inferSelect;
1153
+ /** Row type for brain_weight_history INSERT operations. */
1154
+ export type BrainWeightHistoryInsert = typeof brainWeightHistory.$inferInsert;
1155
+
1156
+ /** Row type for brain_modulators SELECT queries. */
1157
+ export type BrainModulatorRow = typeof brainModulators.$inferSelect;
1158
+ /** Row type for brain_modulators INSERT operations. */
1159
+ export type BrainModulatorInsert = typeof brainModulators.$inferInsert;
1160
+
1161
+ /** Row type for brain_consolidation_events SELECT queries. */
1162
+ export type BrainConsolidationEventRow = typeof brainConsolidationEvents.$inferSelect;
1163
+ /** Row type for brain_consolidation_events INSERT operations. */
1164
+ export type BrainConsolidationEventInsert = typeof brainConsolidationEvents.$inferInsert;
1165
+
794
1166
  // BrainNodeType and BrainEdgeType are declared alongside their enum arrays above.
@@ -178,6 +178,129 @@ function runBrainMigrations(
178
178
  )
179
179
  .run();
180
180
  }
181
+
182
+ // T673-M1: STDP plasticity columns on brain_retrieval_log.
183
+ // session_id was declared in the Drizzle schema but never applied to the live table.
184
+ // reward_signal, retrieval_order, delta_ms are new additions per spec §2.1.1.
185
+ if (tableExists(nativeDb, 'brain_retrieval_log')) {
186
+ ensureColumns(
187
+ nativeDb,
188
+ 'brain_retrieval_log',
189
+ [
190
+ { name: 'session_id', ddl: 'text' },
191
+ { name: 'reward_signal', ddl: 'real' },
192
+ { name: 'retrieval_order', ddl: 'integer' },
193
+ { name: 'delta_ms', ddl: 'integer' },
194
+ ],
195
+ 'brain',
196
+ );
197
+ }
198
+
199
+ // T673-M2: observability columns on brain_plasticity_events
200
+ // session_id is declared in Drizzle schema and included in M2 CREATE TABLE IF NOT EXISTS,
201
+ // but may be missing from installs where the table was created before M2.
202
+ if (tableExists(nativeDb, 'brain_plasticity_events')) {
203
+ ensureColumns(
204
+ nativeDb,
205
+ 'brain_plasticity_events',
206
+ [
207
+ { name: 'session_id', ddl: 'text' },
208
+ { name: 'weight_before', ddl: 'real' },
209
+ { name: 'weight_after', ddl: 'real' },
210
+ { name: 'retrieval_log_id', ddl: 'integer' },
211
+ { name: 'reward_signal', ddl: 'real' },
212
+ { name: 'delta_t_ms', ddl: 'integer' },
213
+ ],
214
+ 'brain',
215
+ );
216
+ }
217
+
218
+ // T673-M3: plasticity tracking columns on brain_page_edges
219
+ ensureColumns(
220
+ nativeDb,
221
+ 'brain_page_edges',
222
+ [
223
+ { name: 'last_reinforced_at', ddl: 'text' },
224
+ { name: 'reinforcement_count', ddl: 'integer NOT NULL DEFAULT 0' },
225
+ { name: 'plasticity_class', ddl: "text NOT NULL DEFAULT 'static'" },
226
+ { name: 'last_depressed_at', ddl: 'text' },
227
+ { name: 'depression_count', ddl: 'integer NOT NULL DEFAULT 0' },
228
+ { name: 'stability_score', ddl: 'real' },
229
+ ],
230
+ 'brain',
231
+ );
232
+
233
+ // T673-M3: seed co_retrieved edges as hebbian (idempotent)
234
+ if (tableExists(nativeDb, 'brain_page_edges')) {
235
+ nativeDb
236
+ .prepare(
237
+ `UPDATE brain_page_edges SET plasticity_class = 'hebbian'
238
+ WHERE edge_type = 'co_retrieved' AND plasticity_class = 'static'`,
239
+ )
240
+ .run();
241
+ }
242
+
243
+ // T673-M4: new plasticity infrastructure tables — self-healing CREATE IF NOT EXISTS.
244
+ // These guards ensure the tables exist even on installs where the Drizzle migration
245
+ // journal was already partially applied. All three tables are CREATE IF NOT EXISTS
246
+ // so re-running is safe.
247
+ nativeDb.exec(
248
+ `CREATE TABLE IF NOT EXISTS brain_weight_history (
249
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
250
+ edge_from_id TEXT NOT NULL,
251
+ edge_to_id TEXT NOT NULL,
252
+ edge_type TEXT NOT NULL,
253
+ weight_before REAL,
254
+ weight_after REAL NOT NULL,
255
+ delta_weight REAL NOT NULL,
256
+ event_kind TEXT NOT NULL,
257
+ source_plasticity_event_id INTEGER,
258
+ retrieval_log_id INTEGER,
259
+ reward_signal REAL,
260
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
261
+ )`,
262
+ );
263
+ nativeDb.exec(
264
+ `CREATE INDEX IF NOT EXISTS idx_weight_history_edge
265
+ ON brain_weight_history (edge_from_id, edge_to_id, edge_type)`,
266
+ );
267
+ nativeDb.exec(
268
+ `CREATE INDEX IF NOT EXISTS idx_weight_history_changed_at
269
+ ON brain_weight_history (changed_at)`,
270
+ );
271
+
272
+ nativeDb.exec(
273
+ `CREATE TABLE IF NOT EXISTS brain_modulators (
274
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
275
+ modulator_type TEXT NOT NULL,
276
+ valence REAL NOT NULL,
277
+ magnitude REAL NOT NULL DEFAULT 1.0,
278
+ source_event_id TEXT,
279
+ session_id TEXT,
280
+ description TEXT,
281
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
282
+ )`,
283
+ );
284
+ nativeDb.exec(
285
+ `CREATE INDEX IF NOT EXISTS idx_modulators_session
286
+ ON brain_modulators (session_id)`,
287
+ );
288
+
289
+ nativeDb.exec(
290
+ `CREATE TABLE IF NOT EXISTS brain_consolidation_events (
291
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
292
+ trigger TEXT NOT NULL,
293
+ session_id TEXT,
294
+ step_results_json TEXT NOT NULL,
295
+ duration_ms INTEGER,
296
+ succeeded INTEGER NOT NULL DEFAULT 1,
297
+ started_at TEXT NOT NULL DEFAULT (datetime('now'))
298
+ )`,
299
+ );
300
+ nativeDb.exec(
301
+ `CREATE INDEX IF NOT EXISTS idx_consolidation_events_started_at
302
+ ON brain_consolidation_events (started_at)`,
303
+ );
181
304
  }
182
305
 
183
306
  /**
@@ -33,6 +33,7 @@ import { checkGlobalSchemas, type CheckResult as SchemaCheckResult } from '../sc
33
33
  import { getAccessor } from '../store/data-accessor.js';
34
34
  import {
35
35
  type CheckResult,
36
+ checkCanonicalRcasdPaths,
36
37
  checkCleoGitignore,
37
38
  checkCoreFilesNotIgnored,
38
39
  checkLegacyAgentOutputs,
@@ -824,11 +825,13 @@ export async function coreDoctorReport(projectRoot: string): Promise<DoctorRepor
824
825
  }
825
826
  }
826
827
 
827
- // 5. Gitignore integrity, vital files, legacy paths (delegated to core checks)
828
+ // 5. Gitignore integrity, vital files, legacy paths, canonical paths (delegated to core checks)
828
829
  checks.push(mapCheckResult(checkCleoGitignore(projectRoot)));
829
830
  checks.push(mapCheckResult(checkVitalFilesTracked(projectRoot)));
830
831
  checks.push(mapCheckResult(checkCoreFilesNotIgnored(projectRoot)));
831
832
  checks.push(mapCheckResult(checkLegacyAgentOutputs(projectRoot)));
833
+ // ADR-045: canonical RCASD paths (T708)
834
+ checks.push(mapCheckResult(checkCanonicalRcasdPaths(projectRoot)));
832
835
 
833
836
  // 5b. Isolated .cleo/.git checkpoint repo check (T4872)
834
837
  const cleoGitHeadExists = existsSync(join(cleoDir, '.git', 'HEAD'));