skynet-graph 1.2.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 (213) hide show
  1. package/CONTRIBUTING.md +22 -0
  2. package/LICENSE +661 -0
  3. package/README.md +245 -0
  4. package/bin/sg +20 -0
  5. package/concepts/README.md +23 -0
  6. package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
  7. package/concepts/_substrate/Claim/Confidence.json +10 -0
  8. package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
  9. package/concepts/_substrate/Claim/Freshness.json +5 -0
  10. package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
  11. package/concepts/_substrate/Claim/Verification.json +6 -0
  12. package/concepts/_substrate/Claim.json +4 -0
  13. package/concepts/_substrate/Frontier/InBeam.json +6 -0
  14. package/concepts/_substrate/Frontier/Stuck.json +6 -0
  15. package/concepts/_substrate/Frontier.json +5 -0
  16. package/concepts/_substrate/Intake/ToTask.json +22 -0
  17. package/concepts/_substrate/Intake/Typed.json +10 -0
  18. package/concepts/_substrate/Intake.json +27 -0
  19. package/concepts/_substrate/Task/Answer.json +12 -0
  20. package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
  21. package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
  22. package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
  23. package/concepts/_substrate/Task/Complexity.json +6 -0
  24. package/concepts/_substrate/Task/EvalComplexity.json +13 -0
  25. package/concepts/_substrate/Task/ReportUp.json +6 -0
  26. package/concepts/_substrate/Task/Rollup.json +10 -0
  27. package/concepts/_substrate/Task.json +5 -0
  28. package/concepts/clinical/Diagnosis/Medication.json +6 -0
  29. package/concepts/clinical/Diagnosis.json +15 -0
  30. package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
  31. package/concepts/clinical/Observation/LabValue.json +5 -0
  32. package/concepts/clinical/Observation.json +6 -0
  33. package/concepts/common/Document/pathBasket.json +6 -0
  34. package/concepts/common/Document.json +3 -0
  35. package/concepts/common/Edge/Distance.json +13 -0
  36. package/concepts/common/Edge/Stay/LongStay.json +8 -0
  37. package/concepts/common/Edge/Stay.json +8 -0
  38. package/concepts/common/Edge/Travel/LongTravel.json +16 -0
  39. package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
  40. package/concepts/common/Edge/Travel/targetNode.json +7 -0
  41. package/concepts/common/Edge/Travel.json +7 -0
  42. package/concepts/common/Edge.json +3 -0
  43. package/concepts/common/Vertice.json +3 -0
  44. package/concepts/supply/Fulfillment.json +10 -0
  45. package/concepts/supply/Inventory/Reorder.json +7 -0
  46. package/concepts/supply/Inventory.json +5 -0
  47. package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
  48. package/concepts/supply/Procurement.json +8 -0
  49. package/concepts/supply/Transport.json +5 -0
  50. package/doc/API.md +556 -0
  51. package/doc/CAPABILITIES.md +373 -0
  52. package/doc/MODELISATION.md +511 -0
  53. package/doc/architecture.md +267 -0
  54. package/doc/concept-as-graph.md +379 -0
  55. package/doc/concept-learning.md +168 -0
  56. package/doc/creative-loop-map.md +202 -0
  57. package/doc/original-2016-doc.md +148 -0
  58. package/doc/usage.md +501 -0
  59. package/lib/authoring/abstract.js +401 -0
  60. package/lib/authoring/abstraction.js +190 -0
  61. package/lib/authoring/adapt.js +393 -0
  62. package/lib/authoring/ancestry.js +216 -0
  63. package/lib/authoring/author.js +147 -0
  64. package/lib/authoring/bounded-merge.js +57 -0
  65. package/lib/authoring/canon.js +158 -0
  66. package/lib/authoring/clock.js +123 -0
  67. package/lib/authoring/combinator.js +92 -0
  68. package/lib/authoring/compete.js +76 -0
  69. package/lib/authoring/compose-hotspot.js +314 -0
  70. package/lib/authoring/compress.js +279 -0
  71. package/lib/authoring/concept-net.js +254 -0
  72. package/lib/authoring/concepts.js +66 -0
  73. package/lib/authoring/context-project.js +277 -0
  74. package/lib/authoring/contract.js +476 -0
  75. package/lib/authoring/corpus-pack.js +125 -0
  76. package/lib/authoring/cost-probe.js +86 -0
  77. package/lib/authoring/crystallize.js +555 -0
  78. package/lib/authoring/dag-decompose.js +256 -0
  79. package/lib/authoring/dataset-adapter.js +253 -0
  80. package/lib/authoring/debug-provider.js +92 -0
  81. package/lib/authoring/decompose.js +343 -0
  82. package/lib/authoring/emittability.js +236 -0
  83. package/lib/authoring/equilibrium.js +160 -0
  84. package/lib/authoring/extract.js +153 -0
  85. package/lib/authoring/forest.js +55 -0
  86. package/lib/authoring/forge-fallback.js +116 -0
  87. package/lib/authoring/givens.js +121 -0
  88. package/lib/authoring/glossary.js +177 -0
  89. package/lib/authoring/grammar-graph.js +191 -0
  90. package/lib/authoring/granularity.js +80 -0
  91. package/lib/authoring/graph-net.js +108 -0
  92. package/lib/authoring/ground.js +148 -0
  93. package/lib/authoring/higher-order.js +85 -0
  94. package/lib/authoring/hotspot.js +132 -0
  95. package/lib/authoring/hysteresis.js +85 -0
  96. package/lib/authoring/index.js +106 -0
  97. package/lib/authoring/lattice-morphism.js +61 -0
  98. package/lib/authoring/lattice-pack.js +148 -0
  99. package/lib/authoring/leaf-io.js +77 -0
  100. package/lib/authoring/library.js +156 -0
  101. package/lib/authoring/lifecycle.js +92 -0
  102. package/lib/authoring/loop.js +193 -0
  103. package/lib/authoring/master-loop.js +132 -0
  104. package/lib/authoring/mdl.js +209 -0
  105. package/lib/authoring/memo-stability.js +122 -0
  106. package/lib/authoring/method-explorer.js +138 -0
  107. package/lib/authoring/method-pack.js +181 -0
  108. package/lib/authoring/method.js +202 -0
  109. package/lib/authoring/mine.js +510 -0
  110. package/lib/authoring/mount.js +119 -0
  111. package/lib/authoring/negotiate.js +100 -0
  112. package/lib/authoring/parametric.js +139 -0
  113. package/lib/authoring/reaggregate.js +75 -0
  114. package/lib/authoring/rebalance.js +157 -0
  115. package/lib/authoring/recall.js +135 -0
  116. package/lib/authoring/registry.js +350 -0
  117. package/lib/authoring/relearn.js +147 -0
  118. package/lib/authoring/retention.js +89 -0
  119. package/lib/authoring/segment-proxy.js +114 -0
  120. package/lib/authoring/serve-leaf.js +64 -0
  121. package/lib/authoring/slot-aware-serve.js +77 -0
  122. package/lib/authoring/sound-invoke.js +59 -0
  123. package/lib/authoring/split-serve.js +74 -0
  124. package/lib/authoring/ste.js +239 -0
  125. package/lib/authoring/stock.js +101 -0
  126. package/lib/authoring/store.js +94 -0
  127. package/lib/authoring/supervise.js +138 -0
  128. package/lib/authoring/support.js +131 -0
  129. package/lib/authoring/task-mirror.js +51 -0
  130. package/lib/authoring/typed-loop.js +160 -0
  131. package/lib/authoring/validate.js +569 -0
  132. package/lib/authoring/widen.js +88 -0
  133. package/lib/combos/appliance.js +189 -0
  134. package/lib/combos/critique.js +377 -0
  135. package/lib/combos/defaults.js +86 -0
  136. package/lib/combos/durable-runner.js +115 -0
  137. package/lib/combos/forge.js +209 -0
  138. package/lib/combos/index.js +86 -0
  139. package/lib/combos/learning-library.js +253 -0
  140. package/lib/combos/mixture-serve.js +193 -0
  141. package/lib/combos/plan-loop.js +192 -0
  142. package/lib/combos/proxy-cache.js +272 -0
  143. package/lib/combos/self-mod.js +94 -0
  144. package/lib/durable/audit.js +102 -0
  145. package/lib/durable/checkpoint-store.js +550 -0
  146. package/lib/durable/fold.js +62 -0
  147. package/lib/durable/interpreter.js +242 -0
  148. package/lib/durable/xlate.js +185 -0
  149. package/lib/graph/Graph.js +2486 -0
  150. package/lib/graph/expr.js +307 -0
  151. package/lib/graph/index.js +26 -0
  152. package/lib/graph/log.js +134 -0
  153. package/lib/graph/objects/Concept.js +382 -0
  154. package/lib/graph/objects/Entity.js +503 -0
  155. package/lib/graph/objects/Node.js +57 -0
  156. package/lib/graph/objects/PathMap.js +557 -0
  157. package/lib/graph/objects/Segment.js +100 -0
  158. package/lib/graph/tasks/stabilize.js +59 -0
  159. package/lib/graph/tasks/taskflow.js +247 -0
  160. package/lib/index.js +122 -0
  161. package/lib/load.js +132 -0
  162. package/lib/providers/backends.js +117 -0
  163. package/lib/providers/borderline.js +129 -0
  164. package/lib/providers/cache.js +226 -0
  165. package/lib/providers/canonicalize.js +198 -0
  166. package/lib/providers/constat.js +107 -0
  167. package/lib/providers/geo.js +53 -0
  168. package/lib/providers/index.js +116 -0
  169. package/lib/providers/intake.js +263 -0
  170. package/lib/providers/llm-local.js +88 -0
  171. package/lib/providers/llm.js +236 -0
  172. package/lib/providers/local-host.js +197 -0
  173. package/lib/providers/merge-consistency.js +162 -0
  174. package/lib/providers/nogood.js +149 -0
  175. package/lib/providers/reason-loop.js +223 -0
  176. package/lib/providers/semiring.js +285 -0
  177. package/lib/providers/solver-fork.js +163 -0
  178. package/lib/providers/stats.js +228 -0
  179. package/lib/providers/verify.js +120 -0
  180. package/lib/runtime/index.js +135 -0
  181. package/lib/runtime/invoke-pool.js +92 -0
  182. package/lib/runtime/protocol.js +210 -0
  183. package/lib/runtime/transport-socket.js +77 -0
  184. package/lib/runtime/worker-entry.js +31 -0
  185. package/lib/sg/cli.js +601 -0
  186. package/lib/sg/log-sinks.js +214 -0
  187. package/lib/sg/mcp.js +395 -0
  188. package/lib/sg/proxy-run.js +66 -0
  189. package/lib/sg/serve-demo.js +185 -0
  190. package/lib/sg/serve.js +205 -0
  191. package/lib/sg/trace.js +118 -0
  192. package/lib/studio/protocol.js +45 -0
  193. package/lib/studio/public/app.js +310 -0
  194. package/lib/studio/public/components/ConceptEditor.js +43 -0
  195. package/lib/studio/public/components/ConceptTree.js +43 -0
  196. package/lib/studio/public/components/CorpusPanel.js +68 -0
  197. package/lib/studio/public/components/ForkTree.js +36 -0
  198. package/lib/studio/public/components/GrammarGraph.js +99 -0
  199. package/lib/studio/public/components/GraphCanvas.js +156 -0
  200. package/lib/studio/public/components/Inspector.js +48 -0
  201. package/lib/studio/public/components/LearningPanel.js +85 -0
  202. package/lib/studio/public/components/PromptConsole.js +46 -0
  203. package/lib/studio/public/components/ProviderTrace.js +39 -0
  204. package/lib/studio/public/components/SessionSplit.js +47 -0
  205. package/lib/studio/public/components/TilingOverlay.js +40 -0
  206. package/lib/studio/public/components/Timeline.js +57 -0
  207. package/lib/studio/public/index.html +26 -0
  208. package/lib/studio/public/styles.css +185 -0
  209. package/lib/studio/public/ws.js +58 -0
  210. package/lib/studio/server.js +130 -0
  211. package/lib/studio/session.js +397 -0
  212. package/lib/studio/studio.js +149 -0
  213. package/package.json +42 -0
@@ -0,0 +1,2486 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ var TaskFlow = require("./tasks/taskflow");
20
+ var Node = require("./objects/Node");
21
+ var Segment = require("./objects/Segment");
22
+ var PathMap = require("./objects/PathMap");
23
+ var Concept = require("./objects/Concept");
24
+ var Entity = require("./objects/Entity");
25
+ //import conceptMap from "../concepts";
26
+
27
+ var isObject = require('is').object;
28
+ var isArray = require('is').array;
29
+ var isFunction = require('is').fn;
30
+ var isString = require('is').string;
31
+ var { compileExpression } = require('./expr');
32
+
33
+ // Wrap a query (string | array | fn) into a predicate fn(scope) that resolves
34
+ // $refs via scope.getRef — replaces the old `new Function` query compiler.
35
+ function compileScopeQuery ( query, emptyValue ) {
36
+ if ( isFunction(query) ) return query;
37
+ var _q = compileExpression(query, { empty: emptyValue });
38
+ return function ( scope ) {
39
+ return _q(function ( ref ) { return scope.getRef(ref); });
40
+ };
41
+ }
42
+ var shortid = require('shortid');
43
+ var dmerge = require('deepmerge');
44
+ var intersect = require('intersect');
45
+ var arrayDiffer = require('array-differ');
46
+ var { createLogger, defaultLogger } = require('./log');
47
+ // Module-level fallback for the rare spots with no graph in scope. Inside methods
48
+ // we rebind `var debug = this._log` (or `me._log`) so logs route to the in-scope
49
+ // graph's own logger (per-graph, captured by a host's injected sink). See init().
50
+ var debug = defaultLogger;
51
+
52
+ /**
53
+ * serialized format :
54
+ * {
55
+ * spatialEP : (nodeid)
56
+ * conceptMaps : [
57
+ * // ... any serialized concept box (nodes/edge or docs) not serialized
58
+ * ],
59
+ * // or
60
+ * nodes : [
61
+ * {
62
+ * _id : ...
63
+ * ConceptKey1 : true,
64
+ * ConceptKey2 : true,//...
65
+ * }
66
+ * ]
67
+ * segments : [
68
+ * {
69
+ * _id : ...
70
+ * originNode : {node _id}
71
+ * targetNode : {node _id}
72
+ * }
73
+ * ]
74
+ * ]
75
+ * }
76
+ *
77
+ *
78
+ * templates format :
79
+ * example : from adding Airports
80
+ [
81
+ {// **************** add start to airport segment
82
+ "Segment": true,
83
+ "$originNode": "_parent:originNode",
84
+
85
+ "targetNode": "nearbyOriginAirport"
86
+ },
87
+ {
88
+ // **************** add nearby origin airport concept node
89
+ "_id": "nearbyOriginAirport",
90
+ "Node": true,
91
+ "isAirport": true,
92
+ "$nearTo": "_parent:originNode"// ref a value or cbox
93
+ },
94
+ {
95
+ // **************** add long travel flight segment
96
+ "Segment": true,
97
+ "$_id": "_parent", // add concepts/keys to _parent, if no $_id -> original object will be kept
98
+ "LongTravel": true,//
99
+ "Distance":null,// should recalculate distance as soon as nearby airport have Position
100
+ "originNode": "nearbyOriginAirport",
101
+ "targetNode": "nearbyTargetAirport"
102
+ },
103
+ {
104
+ // **************** add nearby target airport concept node
105
+ "_id": "nearbyTargetAirport",
106
+ "Node": true,
107
+ "isAirport": true,
108
+ "$nearTo": "_parent:targetNode"
109
+ },
110
+ {
111
+ // **************** add final nearby target airport TO target place
112
+ "Segment": true,
113
+ "originNode": "nearbyTargetAirport",
114
+ "$targetNode": "_parent:targetNode"
115
+ }
116
+ ]
117
+
118
+ */
119
+ function Graph() {
120
+ this.init(...arguments);
121
+ };
122
+
123
+ //Graph._providers = require("../providers");
124
+
125
+ Graph.PathMap = PathMap;
126
+ Graph.Entity = Entity;
127
+ Graph.Concept = Concept;
128
+
129
+ Graph.prototype = {
130
+ static: Graph,
131
+ cfg : {
132
+ label : "graph",
133
+ autoMount : true,
134
+ isMaster : true,
135
+ onStabilize : undefined, // should trigger synchronisation between graphs
136
+ onConceptApply: undefined, // (record) => {} : concept-apply trace sink (see traceProvider)
137
+ conceptSets : ["common"],
138
+ defaultContext: "UserRecord",
139
+ bagRefManagers: {
140
+ caipi: {
141
+ test: /^db\:(.+)$/,
142
+ int : {
143
+ get( refId, cb ) {
144
+ refId = refId.split('#');
145
+ // `App/db` is an optional HOST module (not shipped by the engine). The
146
+ // default 'caipi' manager degrades gracefully (cb error) if the host hasn't
147
+ // provided it — pass cfg.bagRefManagers to supply your own data source.
148
+ var db;
149
+ try { db = require('App/db'); }
150
+ catch ( e ) { return cb(new Error("bagRefManager 'caipi': host module 'App/db' not available; pass cfg.bagRefManagers"), null); }
151
+ db.get(refId[0], refId[1], cb);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ },
157
+ /**
158
+ *
159
+ * @param serialized
160
+ * @param conf
161
+ */
162
+ init: function ( record, conf, conceptMap ) {
163
+ var concepts = {}, me = this,
164
+ serialized;
165
+ if ( isString(record.graph) ) {
166
+ serialized = JSON.parse(record.graph);
167
+ }
168
+ else {
169
+ serialized = record;
170
+ record = {
171
+ lastRev: serialized.lastRev,
172
+ graph : JSON.stringify(record),
173
+ //bagRefs: {}
174
+ }
175
+ }
176
+
177
+ this._triggeredCast = {};
178
+ this._stabilizing = false;// true while a stabilization pass is in flight (see stabilize.js / _applyStabilized)
179
+ this._pendingStructural = [];// add/patchConcept issued mid-stabilize, drained at the quiescent _loopTF boundary (#11.a)
180
+ this._pendingRollback = null;// rollbackTo issued mid-stabilize, applied at the quiescent boundary (#11.c.4)
181
+ this.cfg = { ...this.cfg, ...conf };
182
+ // One logger per graph (replaces the old console indirection). cfg.logger lets a
183
+ // host inject its own; else build one at cfg.logLevel (or env SG_LOG_LEVEL), with
184
+ // cfg.onLog as a convenience sink. Exposed as graph.logger (see below the prototype).
185
+ this._log = this.cfg.logger || createLogger({ label: this.cfg.label, level: this.cfg.logLevel, onRecord: this.cfg.onLog });
186
+ this._applyId = 0;// monotonic id minted per concept-apply: correlates an apply's logs with its trace record (see Concept.applyTo)
187
+ this._applyCount = {};// per-(target/concept) apply tally within an episode; reset on settle (#11.c.1)
188
+ this._applyCap = this.cfg.applyCap || 1000;// oscillation backstop ceiling (per target×concept per episode)
189
+ this.cfg.conceptSets.map(( k ) => concepts = dmerge(concepts, conceptMap[k]));
190
+ me._conceptMap = conceptMap;// kept so fork() can seed children with the same library
191
+ me._conceptLib = {};
192
+ me._syncTokens = {};
193
+ me._syncTokensList = [];
194
+
195
+ this._lastSyncRecord = record;
196
+
197
+ this._rootConcept = new Concept(concepts, me);
198
+ this._mapsByConcept = {};
199
+ this._statsByProvider = {};
200
+ this._bagRefsByRefId = {};
201
+ this._on = {};
202
+ this._revs = [];
203
+ this._revByIds = {};
204
+ this._rev = serialized.lastRev || 0;
205
+ me._triggeredCastCount = 0;
206
+ // TaskFlow (will handle all the graph tasks/mutations)
207
+
208
+ this._taskFlow = new TaskFlow(
209
+ [
210
+ require('./tasks/stabilize')
211
+ ],
212
+ this
213
+ ).then(this._loopTF);
214
+ this._preloadBagRefs(
215
+ serialized.bagRefs || {},
216
+ () => {
217
+ serialized && this.mount(serialized);
218
+
219
+ if ( this.cfg.autoMount ) {
220
+ this._taskFlow.run();
221
+ me._running = true;
222
+ }
223
+ else {
224
+ setTimeout(this._applyStabilized.bind(this));
225
+ }
226
+ }
227
+ );
228
+ },
229
+ /**
230
+ *
231
+ * @param newRefs {"$db:id_from_somwhere":{count:1}}
232
+ * @param cb
233
+ * @private
234
+ */
235
+ _preloadBagRefs( newRefs, cb ) {
236
+ // internal : {"$db:id_from_somwhere":{count:1, lastUpdated:tm, watch:tm, record}}
237
+ var debug = this._log;// per-graph logger; the nested arrows below capture it
238
+ let refsMap = this._bagRefsByRefId,
239
+ mngrs = this.cfg.bagRefManagers,
240
+ running = 1,
241
+ tm,
242
+ refs = Object.keys(newRefs),
243
+ check = () => {
244
+ debug.warn("timeout Preloading refs", newRefs);
245
+ tm = setTimeout(check, 5000);
246
+ },
247
+ done = () => {
248
+ if ( !--running ) {
249
+ //refs.length && debug.log("done Preloading refs", refs);
250
+ clearTimeout(tm)
251
+ //this._taskFlow.release()
252
+ cb()
253
+ }
254
+ };
255
+ //this._taskFlow.wait()
256
+ tm = setTimeout(check, 5000);
257
+ //refs.length && debug.log("Preloading ", refs);
258
+ refs.forEach(
259
+ id => {
260
+ let refMngrId = id && this._isBagRefs(id), v;
261
+ if ( refMngrId && !refsMap[id] ) {
262
+ v = ('' + id).match(mngrs[refMngrId].test)
263
+ //refsMap[id] = refsMap[id] || {
264
+ // count: 0
265
+ //};
266
+ running++;
267
+ mngrs[refMngrId].int.get(
268
+ v[1] || id,
269
+ ( e, r ) => {
270
+ if ( e )
271
+ debug.error("can't retrieve bagRef %s", id);
272
+ refsMap[id] = refsMap[id] || {
273
+ count: 0
274
+ };
275
+ refsMap[id].count++;
276
+ refsMap[id].lastUpdated = Date.now();
277
+ refsMap[id].record = r || { name: "Error" };
278
+ done()
279
+ }
280
+ )
281
+ }
282
+ }
283
+ )
284
+
285
+ done();
286
+ },
287
+ _isBagRefs( id ) {
288
+ if ( id == null || id === '' ) return false;// fast-path: a ref hop's key is most often absent — skip the per-manager regex loop on the hot path
289
+ let refsMap = this._bagRefsByRefId,
290
+ mngrs = this.cfg.bagRefManagers,
291
+ mKeys = Object.keys(mngrs);
292
+ for ( var i = 0, v; i < mKeys.length; i++ ) {
293
+
294
+ v = ('' + id).match(mngrs[mKeys[i]].test)
295
+ if ( v ) {
296
+ return mKeys[i]
297
+ }
298
+ }
299
+ return false;
300
+ },
301
+ // -------------------------------------------------------------------------- core
302
+
303
+ /**
304
+ * Make the stabilisation taskflow loop until theres no more unstable items
305
+ * @param me
306
+ * @param flow
307
+ * @private
308
+ */
309
+ _loopTF : function ( me, flow ) {
310
+ if ( me._dead ) return;
311
+ var debug = me._log;// per-graph logger (captured by the setTimeout closure below)
312
+ // flow.running=false;
313
+
314
+
315
+ // me._running = me.cfg.autoMount;
316
+ setTimeout(function () {// loop
317
+ if ( me._dead ) return;
318
+
319
+ flow.reset();
320
+ debug.info("stabilize loop: %s unstable, %s triggered", me._unstable.length, me._triggeredCastCount);
321
+ flow.then(me._loopTF);
322
+ // me.cfg.autoMount &&
323
+ (me._triggeredCastCount || me._unstable.length) && flow.run();
324
+ });
325
+ if ( !me._unstable.length && !me._triggeredCastCount ) {
326
+ // quiescent boundary. A rollback requested mid-stabilize supersedes everything —
327
+ // it re-mounts an earlier rev (and its concept-lib), so queued structural edits
328
+ // (issued after that rev) are discarded (#11.c.4).
329
+ if ( me._pendingRollback != null ) {
330
+ var _r = me._pendingRollback;
331
+ me._pendingRollback = null;
332
+ me._pendingStructural = [];
333
+ me._doRollback(_r);// mount + restore, no kick — the re-arm re-stabilizes
334
+ return;
335
+ }
336
+ // else apply any structural ops (add/patchConcept) issued mid-stabilize, against
337
+ // the now-settled, consistent cast-state — then let the loop re-run (they
338
+ // write/destabilize) rather than declaring stable (#11.a).
339
+ if ( me._pendingStructural && me._pendingStructural.length ) {
340
+ me._drainStructural();
341
+ // A drain that changed cast-state destabilized objects -> the scheduled loop
342
+ // iteration runs the flow and re-enters here, re-checking quiescence. But a
343
+ // NO-OP drain (the patched concept's re-eval changes no cast — e.g. a tightening
344
+ // that excludes an object already un-cast for another reason) leaves the graph
345
+ // quiescent with nothing to run: the loop never re-arms and `_stabilizing` stays
346
+ // stuck true, so the `stabilize` event / a waiting ingest cb never fire. Only
347
+ // defer when the drain created work; otherwise fall through and settle.
348
+ if ( me._unstable.length || me._triggeredCastCount ) return;
349
+ }
350
+ debug.log("stabilize loop quiescent — settling");
351
+ me._applyStabilized();
352
+ }
353
+ },
354
+ printStats: function () {
355
+ var debug = this._log;
356
+ let stats = this._statsByProvider;
357
+ if ( !stats )
358
+ return;
359
+ let total = 0,
360
+ parts = {},
361
+ results = Object.keys(stats)
362
+ .sort(( a, b ) => (stats[b] - stats[a]))
363
+ .map(( a ) => {
364
+ total += stats[a];
365
+ return a
366
+ })
367
+ .map(( a, i ) => {
368
+ let insec = stats[a] / 1000,
369
+ pct = stats[a] * 100 / total;
370
+ pct = Math.round(pct * 1000) / 1000;
371
+ insec = Math.round(insec * 10) / 10;
372
+ return "\t" + (pct) + "%\t" + "( ~ " + (insec) + "s )\t:\t" + a;
373
+ }).join("\n");
374
+ debug.warn(
375
+ "____________________________________________________")
376
+ debug.warn(
377
+ "%s : Graph providers outer-stats total execTm [ %d s ] \n", this.cfg.label, ~~(total / 1000), results);
378
+ debug.warn(
379
+ "____________________________________________________")
380
+ },
381
+ /**
382
+ * get a serialized json copy of the graph
383
+ * @returns {{spatialEP: (*|string), servicesEP: *, timeStepEP: *, lastSpecified: (*|string), conceptMaps: Array}}
384
+ */
385
+ serialize: function () {
386
+ var state = this._lastSyncState,
387
+ map = this._objById;
388
+ return {
389
+ ...this._lastSyncRecord,
390
+ lastRev: this.getCurrentRevision(),
391
+ graph : JSON.stringify(
392
+ {
393
+ spatialEP : state.spatialEP,
394
+ lastRev : this.getCurrentRevision(),
395
+ conceptMaps: Object.keys(this._objById).map(
396
+ function ( id ) {
397
+ return { ...map[id]._etty._ };
398
+ }
399
+ ),
400
+ bagRefs : Object.keys(this._bagRefsByRefId).reduce(
401
+ ( r, id ) => {
402
+ r[id] = {
403
+ count: this._bagRefsByRefId[id].count
404
+ }
405
+ return r;
406
+ }, {}
407
+ ),
408
+
409
+ }
410
+ )
411
+ }
412
+ },
413
+ /**
414
+ * Do mount the graph (instantiate all objects & mark them as unstable)
415
+ * @param sg serialized graph
416
+ */
417
+ mount : function ( sg, cfg ) {
418
+ var debug = this._log;
419
+ var me = this, stack = [];
420
+
421
+ // copy original state
422
+
423
+ this._lastSyncState = sg = { ...sg };
424
+ this._triggeredCast = {};
425
+ // clean up / init ...
426
+ // user & datas open request
427
+ this._userQuery = [];
428
+ this._dataQuery = [];
429
+
430
+ this._rev = sg.lastRev || 1;
431
+ this._history = [];
432
+ this._unstable = [];
433
+ this._pending = [];
434
+ this._stable = [];
435
+ this._objById = {};
436
+ this._pendingMutationsById = {};
437
+ this.refMap = {};// per-instance (was declared on Graph.prototype → shared across every Graph in the process + grew unbounded)
438
+ sg.freeNodes = sg.freeNodes || [];
439
+ sg.nodes = sg.nodes || [];
440
+ sg.segments = sg.segments || [];
441
+ if ( sg.conceptMaps ) {
442
+ sg.conceptMaps.map(
443
+ function ( map ) {
444
+ if ( !map ) return debug.warn(sg.conceptMaps);
445
+ if ( map.Node ) sg.nodes.push(map);
446
+ else if ( map.Segment ) sg.segments.push(map);
447
+ else sg.freeNodes.push(map);
448
+ }
449
+ );
450
+ }
451
+
452
+ this._freeNodes = sg.freeNodes// free nodes are concept map / scope, linkable on node's & segment's conceptMap, on which we can cast
453
+ // concepts,
454
+ && sg.freeNodes.map(function ( v ) {
455
+ me._objById[v._id] = { _etty: new Entity(v, me) };
456
+ me._unstable.push(me._objById[v._id]) || me._stable.push(me._objById[v._id]);
457
+ me._objById[v._id]._etty.updateApplicableConcepts(me);
458
+
459
+
460
+ return me._objById[v._id];
461
+ })
462
+ || [];
463
+ this._nodes = sg.nodes// nodes first as segments autoregister them in nodes
464
+ && sg.nodes.map(function ( v ) {
465
+ me._objById[v._id] = new Node(v, me);
466
+ // me.cfg.autoMount &&
467
+ me._unstable.push(me._objById[v._id]) || me._stable.push(me._objById[v._id]);
468
+ me._objById[v._id]._etty.updateApplicableConcepts(me);
469
+
470
+ return me._objById[v._id];
471
+ })
472
+ || [];
473
+ this._segments = sg.segments
474
+ && sg.segments.map(function ( v ) {
475
+ me._objById[v._id] = new Segment(v, me);
476
+ // me.cfg.autoMount &&
477
+ me._unstable.push(me._objById[v._id]) || me._stable.push(me._objById[v._id]);
478
+ me._objById[v._id]._etty.updateApplicableConcepts(me);
479
+ return me._objById[v._id];
480
+ })
481
+ || [];
482
+ debug.verbose("graph mounted (%s objects)", Object.keys(this._objById || {}).length);
483
+
484
+ },
485
+ // refId -> [walked ref ids], so a multi-hop walk can tear down its old watchers
486
+ // when an intermediate ref is repointed (see the `refs[exp.length]` handling below).
487
+ refMap: {},
488
+ /**
489
+ * Resolve a reference expression against a scope object — the read half of the
490
+ * `$`-ref mini-DSL (the same syntax used in mutation templates and assert/query
491
+ * strings). Optionally installs a watcher so the scope is retested when the
492
+ * resolved value later changes/appears.
493
+ *
494
+ * The expression is a chain of two kinds of step:
495
+ * `.` walk INTO a key of the current object (Position.lat)
496
+ * `:` follow a reference to ANOTHER object, then keep walking
497
+ * (originNode:Position.lat → hop to the origin node, read its Position.lat)
498
+ * `$` prefix = a GLOBAL lookup by id in _objById ($paris, $paris:Position)
499
+ * A trailing `:` (e.g. "originNode:") returns the linked OBJECT itself, not a value.
500
+ * A bare key ("Distance") returns that fact off the scope.
501
+ *
502
+ * `follow`:
503
+ * - `true` → if the value is absent, destabilize `scope`'s object so it is
504
+ * retested once the value appears (this is how `require` waits on a
505
+ * not-yet-produced fact); internally normalized to the scope id.
506
+ * - a fn → call it on change instead of destabilizing.
507
+ * - falsy → plain read, no watcher.
508
+ * `unref` removes a previously-installed watcher instead of adding one.
509
+ * `getBox` resolves an id → Entity (defaults to this.getEtty; overridable for tests).
510
+ *
511
+ * bagRefs (external data ids matching cfg.bagRefManagers) resolve to the preloaded
512
+ * record in _bagRefsByRefId rather than to a graph object.
513
+ *
514
+ * @param {string} exp ref expression (e.g. "originNode:Position.lat")
515
+ * @param {Entity|string} scope object (or its id) the expression is relative to
516
+ * @param {boolean|fn} [follow] install a watcher (see above)
517
+ * @param {boolean} [unref] remove the watcher instead
518
+ * @param {fn} [getBox] id -> Entity resolver
519
+ * @returns the resolved value, or the linked Entity (trailing `:`), or undefined
520
+ */
521
+ getRef: function ( exp, scope, follow, unref, getBox ) {
522
+ var debug = this._log;
523
+ let cScope = isString(scope) && this.getEtty(scope) || scope || this.getEtty(this.cfg.defaultContext),
524
+ refId = cScope && cScope._ && (cScope._._id + '::' + exp),
525
+ keyRefId,
526
+ refStack;
527
+
528
+ getBox = getBox || this.getEtty.bind(this);
529
+
530
+ exp = exp.split('.');
531
+ follow = follow === true && scope._._id || follow;
532
+
533
+
534
+ let refs = this.refMap[refId] = follow && !unref && this.refMap[refId] || [], bagRef;
535
+
536
+ while ( exp.length ) {
537
+ if ( exp[0].indexOf(':') != -1 ) {// follow the ref
538
+ exp[0] = exp[0].split(':');
539
+
540
+ while ( exp[0].length != 1 ) {// consume each `:`-hop, switching scope to the linked object
541
+
542
+ if ( exp[0][0][0] == '$' ) {// global ref
543
+ cScope = getBox(exp[0][0].substr(1));// switch scope
544
+ exp[0].shift();
545
+ }
546
+ else {
547
+ // read the id stored under this key, then hop to the object it names
548
+ keyRefId = cScope.get && cScope.get(exp[0][0], follow, unref) || cScope[exp[0][0]];// walk
549
+ bagRef = this._isBagRefs(keyRefId)
550
+
551
+ if ( bagRef ) // the id points at external (bagRef) data, not a graph object
552
+ {
553
+ if ( !this._bagRefsByRefId[keyRefId] )
554
+ debug.warn("ref to an unknown bagRef %s", keyRefId, cScope)
555
+ cScope = this._bagRefsByRefId[keyRefId] && this._bagRefsByRefId[keyRefId].record || null;
556
+ }
557
+ else {
558
+ // if this hop was repointed since last time, drop the stale watcher
559
+ // installed on the previous target's key before following the new one
560
+ if ( follow && !unref && refs[exp.length] && (refs[exp.length] != keyRefId) )
561
+ getBox(refs[exp.length]) &&
562
+ getBox(refs[exp.length])._etty.get(exp[0][0], follow, true);
563
+
564
+ cScope = getBox(keyRefId);// walk to the linked object
565
+ }
566
+ exp[0].shift();
567
+
568
+ }
569
+ if ( !cScope ) return;
570
+ }
571
+ if ( !exp[0][0] ) {
572
+ return cScope;// exp finishig by : ex: "originNode:"
573
+ }
574
+ exp[0] = exp[0][0];
575
+ }
576
+ if ( exp[0][0] == '$' ) {// global ref
577
+ if ( exp.length == 1 )
578
+ return this._objById[exp[0].substr(1)] && this._objById[exp[0].substr(1)]._etty._._id;
579
+ cScope = this._objById[exp[0].substr(1)] && this._objById[exp[0].substr(1)]._etty;
580
+ }
581
+ else if ( exp.length == 1 ) {
582
+ return cScope.get ? cScope.get(exp[0], follow, unref) : cScope[exp[0]];
583
+
584
+ }
585
+ else if ( exp.length ) {
586
+ cScope = cScope.get ? cScope.get(exp[0], follow, unref) : cScope[exp[0]];
587
+ }
588
+ if ( !cScope ) return;
589
+ exp.shift();
590
+ }
591
+ return cScope;
592
+ },
593
+
594
+ update: function ( record ) {
595
+ var debug = this._log;
596
+ var cRecord = this._lastSyncRecord,
597
+ changes = Object.keys(record).map(( b ) => ((record[b] != cRecord[b]) && b)).filter(
598
+ i => !!i || ["graph", "updated"].includes(i)
599
+ );
600
+
601
+ if ( !changes.length || (changes.length == 1) && changes[0] == "updated" ) {
602
+
603
+ return;// no changes
604
+ }
605
+ else {
606
+ debug.error(changes);
607
+ if ( this._rev <= record.lastRev ) {
608
+
609
+ this._revs[this._rev] = {
610
+ id : shortid.generate(),
611
+ recordUpdate: record
612
+ };
613
+ record.lastRev++;
614
+ this._rev++;
615
+ // this.stabilize();
616
+
617
+
618
+ }
619
+
620
+ this._lastSyncRecord = {
621
+ ...this._lastSyncRecord,
622
+ ...record
623
+ };
624
+ }
625
+ },
626
+
627
+ // -------------------------------------------------------------------------- atomic stuff
628
+
629
+ getCurrentRevision: function () {
630
+ return this._rev;
631
+ },
632
+ /**
633
+ * get all atoms from from to to
634
+ * @param from
635
+ * @param to
636
+ * @returns {Array.<T>}
637
+ */
638
+ getRevisionsRange: function ( from, to ) {
639
+ this._log.verbose("getRevisionsRange %s..%s", from, to);
640
+ return this._revs.slice(from, to);
641
+ },
642
+
643
+
644
+ _mutationThread : [],
645
+ _mutationThreadRunning: false,
646
+ _atomicThread : [],
647
+ _atomicThreadRunning : false,
648
+ /**
649
+ * Push atoms from remote or client
650
+ * @param from
651
+ * @param to
652
+ * @param atoms
653
+ * @param token
654
+ * @param resetRevs
655
+ */
656
+ pushAtomicUpdates: function ( from, to, atoms, token, resetRevs ) {
657
+ // !__SERVER__ &&
658
+ var debug = this._log;
659
+ var me = this,
660
+ i = 0,
661
+ max = to - from,
662
+ allRefs = {};
663
+ //
664
+ //if ( this._atomicThreadRunning ) {
665
+ //
666
+ // return this._atomicThread.push([...arguments]);
667
+ //}
668
+ //this._atomicThreadRunning = true;
669
+ debug.warn('RT Update request:', from, to, token, !__SERVER__ && atoms);
670
+ //while ( i < max ) {
671
+ // atoms[i]
672
+ // && atoms[i].bagRefs
673
+ // && atoms[i].bagRefs.length
674
+ // && atoms[i].bagRefs.each(id => {
675
+ // allRefs[id] = allRefs[id] || { count: 0 };
676
+ // allRefs[id].count++;
677
+ // });
678
+ // i++;
679
+ //}
680
+ //this._preloadBagRefs(
681
+ // allRefs,
682
+ // () => {
683
+ // me._inited = true;
684
+ while ( i < max ) {
685
+ atoms[i]
686
+ //&& !this._revByIds[atoms[i].id]//@todo: alpha method...
687
+ && this.pushMutation(atoms[i].tpl, atoms[i].parent, true, atoms[i].id, atoms[i].bagRefs), i++;
688
+ }
689
+ token = isArray(token) ? token : token && [token] || [];
690
+ // token && this.on("stabilize", function fn() {// sync cb
691
+ // me.un("stabilize", fn);
692
+ //
693
+ // });
694
+ // if ( this.cfg.autoMount ) {
695
+ if ( !this._taskFlow.running ) {
696
+ this._taskFlow.run();
697
+ }
698
+ me._running = true;
699
+ token.map(( t ) => me._syncTokensList.push(t));
700
+ // } else this._applyStabilized();
701
+
702
+ token.map(( t ) => me._syncTokens[t] && me._syncTokens[t]());
703
+ this.stabilize(
704
+ //r=>{
705
+ //
706
+ //}
707
+ );
708
+
709
+ this._on.atomicUpdate
710
+ && this._on.atomicUpdate.map(( cb ) => cb(me, from, to, atoms));
711
+ this.cfg.onAtomicUpdate
712
+ && this.cfg.onAtomicUpdate(this, from, to, atoms);
713
+ //this._atomicThreadRunning = false;
714
+ //
715
+ //if ( this._atomicThread.length ) {
716
+ // this.pushAtomicUpdates(...this._atomicThread.shift())
717
+ //}
718
+ //}
719
+ //)
720
+
721
+ },
722
+
723
+ // -------------------------------------------------------------------------- mutations stuff
724
+
725
+ /**
726
+ * CDC ingestion — push live external-record field changes IN, through the SEQUENCED mutation path.
727
+ *
728
+ * This is the "revive the live-record path" entry point (study §5/B1, B9). The engine's bagRef binding
729
+ * is a read-once SNAPSHOT — correct for a record's STRUCTURAL fields (the decomposition keys on them;
730
+ * they must not churn mid-case). A record's DEFEASIBLE / live fields instead flow in here as ordinary
731
+ * GRAPH FACTS, set on the bound object, so the JTMS re-derives exactly the leaves that designed-in
732
+ * depend on them (a freshness `ensure` retracts a stale leaf; an `ensure` whose value flipped re-casts).
733
+ * That is the B9 structural-snapshot / defeasible-live split, and — crucially — it stays DETERMINISTIC:
734
+ * every change is a mutation TEMPLATE applied through `pushMutation` (taskflow-sequenced, rev-logged, so
735
+ * bisectable/replayable via `pushAtomicUpdates`), never an out-of-band `set`. This mirrors how the
736
+ * original host (epikeo) flowed external data: through the atomic-update / revision path, not by mutating
737
+ * the bagRef record in place.
738
+ *
739
+ * The whole batch is ONE sequenced settle (one rev step), not N — so a multi-record CDC tick is atomic.
740
+ *
741
+ * @param {object|Array} updates either `{ objId: { field: val, ... }, ... }`
742
+ * or `[ { id, fields:{...} }, ... ]`
743
+ * @param {fn} [cb] called once the ingest has settled
744
+ * @returns {Graph} this
745
+ */
746
+ ingest: function ( updates, cb ) {
747
+ var tpl = [];
748
+ if ( isArray(updates) )
749
+ updates.forEach(function ( u ) { u && u.id != null && tpl.push({ ...(u.fields || {}), $$_id: u.id }); });
750
+ else if ( updates )
751
+ Object.keys(updates).forEach(function ( id ) { tpl.push({ ...updates[id], $$_id: id }); });
752
+ if ( !tpl.length ) { cb && setTimeout(cb); return this; }
753
+ this.pushMutation(tpl, null); // one sequenced, rev-logged mutation for the whole batch
754
+ this.stabilize(cb);
755
+ return this;
756
+ },
757
+ /**
758
+ *
759
+ * @param path
760
+ * @param mapLib
761
+ * @param tSegment
762
+ */
763
+ pushMutationFromPath: function ( path, descrs, tSegment ) {
764
+ this.pushMutation(this.getMutationFromPath(path, descrs, tSegment), tSegment)
765
+ },
766
+ /**
767
+ * convert a path to a graph template
768
+ * @param path
769
+ * @param mapLib
770
+ * @param tSegment
771
+ * @returns {Array}
772
+ */
773
+ getMutationFromPath: function ( path, descrs, tSegment ) {
774
+ var
775
+ me = this,
776
+ tpl = path.slice(0);
777
+ tpl.unshift.apply(tpl, descrs);//?
778
+ tpl = tpl.map(
779
+ function ( cmap ) {
780
+ cmap = { ...cmap };
781
+ if ( cmap.__bagRefKeys ) {
782
+ cmap.__bagRefKeys.forEach(
783
+ key => {
784
+ if ( cmap[key] )
785
+ cmap['$$' + key] = cmap[key];
786
+ delete cmap[key];
787
+ }
788
+ )
789
+ delete cmap.__bagRefKeys;
790
+ }
791
+ // if ( cmap.TimePeriod ) {// periods from the vendor record
792
+ // Period.start = Math.min(Period.start, cmap.TimePeriod.start);
793
+ // Period.end = Math.max(Period.end, cmap.TimePeriod.end);
794
+ // }
795
+ // if ( (related = cmap.pathDescriptor) && mapLib[related] ) {
796
+ // do {
797
+ // if ( !relatedRefs[related] ) {
798
+ // relatedRefs[related] = merge(true, mapLib[related]);
799
+ // relatedTpl.push(relatedRefs[related]);
800
+ // }
801
+ // } while (related = mapLib[related] && mapLib[related].parentPathDescriptor)
802
+ // }
803
+ if ( cmap._rev )
804
+ delete cmap._rev;
805
+ // if ( me._objById[cmap._id] ) {// existing turn to ref
806
+ //
807
+ // // if (cmap._id==)
808
+ //
809
+ // cmap.$_id = '$' + cmap._id + '._id';
810
+ // delete cmap._id;
811
+ //
812
+ // }
813
+ return cmap;
814
+ }
815
+ );
816
+ return tpl;
817
+ },
818
+ /**
819
+ * Manual/forced concept uncast
820
+ * @param cmapId the target cmap id
821
+ * @param cId the concept id
822
+ * @param cb
823
+ */
824
+ unCastConcept: function ( cmapId, cId, cb ) {
825
+ if ( this._objById[cmapId]._etty._._autokill ) return;
826
+ var me = this, key = cmapId + '/' + cId;
827
+ if ( this._triggeredCast[key] )
828
+ delete this._triggeredCast[key];
829
+ this.pushMutation(
830
+ {
831
+ $_id : "_parent",
832
+ [this._conceptLib[cId]._name]: null
833
+ },
834
+ cmapId
835
+ );
836
+ // this._objById[cmapId]._etty.unCast(this._conceptLib[cId]._name, null);
837
+ // this.toggleGraphObjectState(cmapId, "unstable");
838
+ this.stabilize(cb);
839
+ },
840
+ /**
841
+ * Manual/forced concept cast
842
+ * @param cmapId the target cmap id
843
+ * @param cId the concept id
844
+ * @param cb
845
+ */
846
+ castConcept : function ( cmapId, cId, cb ) {
847
+ if ( this._objById[cmapId]._etty._._autokill ) return;
848
+ var me = this, key = cmapId + '/' + cId;
849
+ // this._triggeredCast[key] = [cmapId, cId];
850
+ // me._triggeredCastCount++;
851
+ this._taskFlow.pushSubTask(
852
+ this._conceptLib[cId].applyTo(this._objById[cmapId]._etty, this));
853
+ this.toggleGraphObjectState(cmapId, "unstable");
854
+ this.stabilize(cb);
855
+ },
856
+ /**
857
+ * Resolve a concept by its id (`_conceptLib` key) or, failing that, by its
858
+ * `_name`. Returns the Concept instance or null.
859
+ * @param nameOrId
860
+ */
861
+ getConceptByName: function ( nameOrId ) {
862
+ if ( this._conceptLib[nameOrId] ) return this._conceptLib[nameOrId];
863
+ var ids = Object.keys(this._conceptLib);
864
+ for ( var i = 0; i < ids.length; i++ )
865
+ if ( this._conceptLib[ids[i]]._name === nameOrId )
866
+ return this._conceptLib[ids[i]];
867
+ return null;
868
+ },
869
+ /**
870
+ * Hot-patch an expert (concept) and re-evaluate the WHOLE graph against it,
871
+ * in both directions — no restart, no rebuild:
872
+ * - patch the concept's schema + recompile its applicability test
873
+ * (delegated to Concept.patch);
874
+ * - for every live object: newly-applicable + not cast -> castConcept;
875
+ * cast + no-longer-applicable -> unCast (which cascades to children);
876
+ * - re-stabilize.
877
+ *
878
+ * @param nameOrId concept id (`_conceptLib` key) or `_name`
879
+ * @param updates partial concept schema (e.g. `{ assert: ["$x > 5"] }`)
880
+ * @param cb optional stabilize callback
881
+ * @returns {Graph} this
882
+ */
883
+ patchConcept : function ( nameOrId, updates, cb ) {
884
+ // Issued mid-stabilize? Defer to the quiescent _loopTF boundary so the re-eval
885
+ // sees a settled cast-state (else a patch of the concept currently mid-apply is
886
+ // silently dropped — its self-flag is not written yet). (#11.a re-entrancy.)
887
+ if ( this._stabilizing ) {
888
+ this._pendingStructural.push({ op: 'patch', nameOrId: nameOrId, updates: updates, cb: cb });
889
+ return this;
890
+ }
891
+ this._doPatchConcept(nameOrId, updates);
892
+ this.stabilize(cb);
893
+ return this;
894
+ },
895
+ /**
896
+ * The bidirectional re-eval body of patchConcept WITHOUT the stabilize kick.
897
+ * Casts (no-kick: queue the apply + destabilize) newly-applicable objects and
898
+ * uncasts (cascades) no-longer-applicable ones. Used by the public method (host
899
+ * path, one kick at the end) and by the mid-stabilize drain.
900
+ */
901
+ _doPatchConcept: function ( nameOrId, updates ) {
902
+ var me = this,
903
+ concept = this.getConceptByName(nameOrId);
904
+ if ( !concept )
905
+ throw new Error("patchConcept: no concept '" + nameOrId + "'");
906
+
907
+ concept.patch(updates);
908
+ this._conceptSnapshot = null;// concept lib changed — invalidate the N6 schema snapshot cache
909
+
910
+ // #11.b scoped re-eval: only the objects whose cast-state for C could change —
911
+ // not the whole graph (was O(graph) stop-the-world).
912
+ this._scopedReevalIds(concept).forEach(function ( id ) {
913
+ var o = me._objById[id];
914
+ if ( !o ) return;
915
+ var etty = o._etty;
916
+ if ( !etty || etty._dead ) return;
917
+ var applicable = !!concept.isApplicableTo(etty, me),
918
+ isCast = !!etty._mappedConcepts[concept._name];
919
+ if ( applicable && !isCast ) {
920
+ me._taskFlow.pushSubTask(concept.applyTo(etty, me));// no-kick cast
921
+ me.toggleGraphObjectState(id, "unstable");
922
+ }
923
+ else if ( !applicable && isCast ) {
924
+ etty.unCast(concept._name);
925
+ me.toggleGraphObjectState(id, "unstable");
926
+ }
927
+ });
928
+ return concept;
929
+ },
930
+ /**
931
+ * The objects whose cast-state for `concept` could change under a patch — its
932
+ * sound, minimal re-eval frontier (#11.b):
933
+ * - `_mapsByConcept[C._name]` — objects where C is/was cast (the UNCAST direction
934
+ * on a tightening patch);
935
+ * - `_mapsByConcept[r]` for each simple `require` r — objects that carry a fact C
936
+ * needs, so a loosening patch could newly CAST C on them (incl. never-cast ones).
937
+ * Falls back to the full object scan when it can't be soundly scoped: an assert-only
938
+ * concept (no `require` ⇒ any object could apply) or a cross-object walk require
939
+ * (`a:b` ⇒ the require fact lives on another object, not indexable here).
940
+ */
941
+ _scopedReevalIds: function ( concept ) {
942
+ var me = this,
943
+ requires = isArray(concept._schema.require) ? concept._schema.require
944
+ : concept._schema.require && [concept._schema.require] || [];
945
+ if ( !requires.length || requires.some(function ( r ) { return String(r).indexOf(':') !== -1; }) )
946
+ return Object.keys(me._objById);
947
+ var set = {};
948
+ (me._mapsByConcept[concept._name] || []).forEach(function ( id ) { set[id] = 1; });
949
+ requires.forEach(function ( r ) {
950
+ (me._mapsByConcept[r] || []).forEach(function ( id ) { set[id] = 1; });
951
+ });
952
+ return Object.keys(set);
953
+ },
954
+ /**
955
+ * Apply-ceiling backstop (#11.c.1): a (target, concept) pair has applied past the
956
+ * `_applyCap` within one episode. NOTE the framing: a self-destabilizing re-cast loop
957
+ * is NOT inherently pathological — it is a legitimate *iterative-trial* technique
958
+ * (the engine's own way to stabilize / try paths & casts). So the cap is a BACKSTOP
959
+ * (default high, reset on each settle so a converging trial loop is never killed), and
960
+ * `divergent` is the "did not converge within the ceiling" *outcome* — a reusable trial
961
+ * signal, not just an error. (Future: an explicit per-concept iteration budget could
962
+ * turn this into a controlled explore-variations loop — AO-star/beam + the learning loop.)
963
+ *
964
+ * Record WHY — a reason record pushed (race-free `{__push}`) into the target's
965
+ * `divergent` array fact. That fact is a NON-CAST CONDITION (Concept.isApplicableTo
966
+ * reads it), so the concept de-casts and stops re-firing; it is also a retraction
967
+ * trigger a host / meta-concept can `ensure`-gate on. Idempotent (one record per pair).
968
+ */
969
+ _markDivergent: function ( scope, concept, count ) {
970
+ var debug = this._log;
971
+ var cur = scope._ && scope._.divergent;
972
+ if ( cur && cur.length )
973
+ for ( var i = 0; i < cur.length; i++ )
974
+ if ( cur[i] && cur[i].concept === concept._name ) return;// already recorded
975
+ debug.error("DIVERGENT: %s blew the apply ceiling (%s) on %s — de-casting + flagging why",
976
+ concept._name, this._applyCap, scope._._id);
977
+ this.pushMutation({
978
+ $_id : "_parent",
979
+ divergent: { __push: { concept: concept._name, applies: count, cap: this._applyCap, reason: "apply-cap" } }
980
+ }, scope._._id);
981
+ },
982
+ /**
983
+ * Install a NEW expert (concept) into the live library and re-evaluate the
984
+ * graph against it — the symmetric twin of `patchConcept`, no restart/rebuild.
985
+ * This is the engine half of declarative AI-authoring (roadmap #10): an
986
+ * authoring loop proposes a concept term, the host validator gates it, this
987
+ * installs it.
988
+ *
989
+ * It composes existing machinery only (Concept ctor + the cast/sweep/stabilize
990
+ * path), so there is no new evaluation code:
991
+ * - `new Concept` registers itself (and any nested `childConcepts`) into
992
+ * `_conceptLib` keyed by `_id`, and recompiles its applicability test;
993
+ * - it is attached under `parent._openConcepts` keyed by `_id` (the engine
994
+ * invariant: a child's key in `_openConcepts`/`childConcepts` IS its `_id`,
995
+ * because `updateApplicableConcepts` pushes those keys straight into
996
+ * `_conceptLib[...]`), and mirrored into `parent._schema.childConcepts` so
997
+ * serialize() carries the new capability;
998
+ * - every live object under the (cast) parent has the new concept opened in
999
+ * its `_mapOpenConcepts` and is re-swept — exactly how an object discovers
1000
+ * a root concept at mount. Unresolved `require`s install their follow-watcher
1001
+ * during the sweep, so a dormant concept fires when its fact later appears.
1002
+ *
1003
+ * Re-entrancy note: like `patchConcept`, this casts/uncasts + `stabilize()`s
1004
+ * directly (not via the mutation queue). It is meant to be called from the host
1005
+ * at a quiescent boundary (between stabilizations). Calling it from *inside* a
1006
+ * provider mid-stabilize is the self-modification tier (roadmap #11) and needs
1007
+ * the queued/scoped variant (MODELISATION §6.4) — not done here.
1008
+ *
1009
+ * @param parentNameOrId parent concept id/`_name`; null/undefined = top-level
1010
+ * (child of the root container)
1011
+ * @param schema a concept schema: `{ _id, _name?, require?, assert?,
1012
+ * ensure?, provider?, applyMutations?, childConcepts? }`.
1013
+ * `_id` must be globally unique; `_name` defaults to `_id`.
1014
+ * @param cb optional stabilize callback
1015
+ * @returns {Concept} the installed concept
1016
+ */
1017
+ addConcept : function ( parentNameOrId, schema, cb ) {
1018
+ // Issued mid-stabilize (e.g. from a meta-concept's provider)? Defer to the
1019
+ // quiescent _loopTF boundary, where cast-state is settled (#11.a re-entrancy).
1020
+ // The deferred op installs at drain time; its cb gets (err, concept) then.
1021
+ if ( this._stabilizing ) {
1022
+ this._pendingStructural.push({ op: 'add', parentNameOrId: parentNameOrId, schema: schema, cb: cb });
1023
+ return;
1024
+ }
1025
+ var concept = this._doAddConcept(parentNameOrId, schema);
1026
+ this.stabilize(cb);
1027
+ return concept;
1028
+ },
1029
+ /**
1030
+ * The structural body of addConcept WITHOUT the stabilize kick — builds+registers
1031
+ * the concept, attaches it to the parent, opens it on the live objects, and
1032
+ * destabilizes them. Used directly by the public method (host path) and by the
1033
+ * mid-stabilize drain (`_drainStructural`).
1034
+ */
1035
+ _doAddConcept : function ( parentNameOrId, schema ) {
1036
+ var me = this;
1037
+ if ( !schema || !schema._id )
1038
+ throw new Error("addConcept: schema must have a unique _id");
1039
+ if ( this._conceptLib[schema._id] )
1040
+ throw new Error("addConcept: concept '" + schema._id + "' already exists (duplicate _id)");
1041
+ if ( !schema._name ) schema._name = schema._id;
1042
+
1043
+ var parent = parentNameOrId ? this.getConceptByName(parentNameOrId) : this._rootConcept;
1044
+ if ( !parent )
1045
+ throw new Error("addConcept: no parent concept '" + parentNameOrId + "'");
1046
+
1047
+ // build + register (Concept.init does graph._conceptLib[_id]=this, recursively
1048
+ // for nested childConcepts, and compiles the applicability test)
1049
+ var concept = new Concept(schema, this, parent === this._rootConcept ? undefined : parent);
1050
+
1051
+ // attach under the parent's open-concept map (key === _id, the engine invariant)
1052
+ if ( !parent._openConcepts ) {
1053
+ parent._openConcepts = {};
1054
+ parent._openConceptsRequires = {};
1055
+ parent.isLeaf = false;
1056
+ }
1057
+ parent._openConcepts[schema._id] = concept;
1058
+ parent._openConceptsRequires[schema._id] =
1059
+ isArray(schema.require) ? schema.require : schema.require && [schema.require] || [];
1060
+ // mirror into the parent's live schema so serialize()/snapshots carry it
1061
+ parent._schema.childConcepts = parent._schema.childConcepts || {};
1062
+ parent._schema.childConcepts[schema._id] = schema;
1063
+ this._conceptSnapshot = null;// concept lib changed — invalidate the N6 schema snapshot cache
1064
+
1065
+ // open + re-sweep every live object the new concept could apply to: the root
1066
+ // container reaches all objects; a real parent reaches objects it is cast on.
1067
+ var atRoot = parent === this._rootConcept;
1068
+ Object.keys(this._objById).forEach(function ( id ) {
1069
+ var etty = me._objById[id]._etty;
1070
+ if ( !etty || etty._dead ) return;
1071
+ if ( !atRoot && !etty._mappedConcepts[parent._name] ) return;
1072
+ if ( etty._mapOpenConcepts.indexOf(schema._id) === -1 )
1073
+ etty._mapOpenConcepts.push(schema._id);
1074
+ etty.updateApplicableConcepts(me);
1075
+ me.toggleGraphObjectState(id, "unstable");
1076
+ });
1077
+
1078
+ return concept;
1079
+ },
1080
+ /**
1081
+ * REMOVE an expert (concept) from the live library and re-stabilize — the symmetric
1082
+ * inverse of `addConcept`, completing the grammar CRUD (an operator editing a grammar
1083
+ * in the Studio must be able to delete a concept, not only add/patch it). Composes
1084
+ * existing machinery only (`Entity.unCast` + the sweep/stabilize path): no new teardown
1085
+ * code. Deleting a concept with children removes the whole SUBTREE (its descendants are
1086
+ * only reachable under it). Like add/patchConcept it is re-entrant — issued mid-stabilize
1087
+ * it defers to the quiescent `_loopTF` boundary (#11.a).
1088
+ *
1089
+ * @param nameOrId concept id (`_conceptLib` key) or `_name`
1090
+ * @param cb optional stabilize callback
1091
+ * @returns {Graph} this
1092
+ */
1093
+ deleteConcept : function ( nameOrId, cb ) {
1094
+ if ( this._stabilizing ) {
1095
+ this._pendingStructural.push({ op: 'delete', nameOrId: nameOrId, cb: cb });
1096
+ return this;
1097
+ }
1098
+ this._doDeleteConcept(nameOrId);
1099
+ this.stabilize(cb);
1100
+ return this;
1101
+ },
1102
+ /** The concept ids of `concept` and all its descendants (its `_openConcepts` subtree). */
1103
+ _conceptSubtreeIds: function ( concept ) {
1104
+ var ids = [concept._id], oc = concept._openConcepts;
1105
+ if ( oc ) for ( var k in oc ) ids = ids.concat(this._conceptSubtreeIds(oc[k]));
1106
+ return ids;
1107
+ },
1108
+ /**
1109
+ * The structural body of deleteConcept WITHOUT the stabilize kick. UN-CASTS the concept
1110
+ * from every live object (permanent — `unCast(name, _id)` does NOT re-open it, and cascades
1111
+ * to its own CHILDREN), destabilizes the objects it was cast on, detaches it from its parent
1112
+ * (open map + `_schema.childConcepts`, so serialize()/snapshots drop it), and removes the
1113
+ * subtree from `_conceptLib` + the per-concept index. Used by the public method (host path)
1114
+ * and the mid-stabilize drain.
1115
+ *
1116
+ * v0 LIMIT (honest): an already-cast DEPENDENT — a sibling concept that `require`s the deleted
1117
+ * concept's fact — is NOT auto-un-cast (the engine's require-stickiness: a cast concept does not
1118
+ * re-evaluate on a require change, finding #22). The delete removes the TARGET (+ its subtree)
1119
+ * soundly; it will simply not cast on FUTURE objects (its require fact is gone). Cascading to
1120
+ * stale existing dependents would need an explicit dependent re-eval (the patchConcept frontier
1121
+ * pattern) — a follow-up, not done here.
1122
+ */
1123
+ _doDeleteConcept: function ( nameOrId ) {
1124
+ var me = this,
1125
+ concept = this.getConceptByName(nameOrId);
1126
+ if ( !concept )
1127
+ throw new Error("deleteConcept: no concept '" + nameOrId + "'");
1128
+ var subtree = this._conceptSubtreeIds(concept);
1129
+
1130
+ // un-cast from EVERY live object: cast objects retract (+ destabilize so dependents re-eval),
1131
+ // objects where it is merely OPEN (dormant) drop it from their open set (the unReachable filter).
1132
+ Object.keys(this._objById).forEach(function ( id ) {
1133
+ var o = me._objById[id], etty = o && o._etty;
1134
+ if ( !etty || etty._dead ) return;
1135
+ var wasCast = !!etty._mappedConcepts[concept._name];
1136
+ etty.unCast(concept._name, concept._id);// _id ⇒ permanent (no re-open) + cascades to children
1137
+ if ( wasCast ) me.toggleGraphObjectState(id, "unstable");
1138
+ });
1139
+
1140
+ // detach from the parent so serialize()/snapshots no longer carry it
1141
+ var parent = concept._parent || this._rootConcept;
1142
+ if ( parent._openConcepts ) delete parent._openConcepts[concept._id];
1143
+ if ( parent._openConceptsRequires ) delete parent._openConceptsRequires[concept._id];
1144
+ if ( parent._schema && parent._schema.childConcepts ) delete parent._schema.childConcepts[concept._id];
1145
+
1146
+ // drop the subtree from the registry + the per-concept index
1147
+ subtree.forEach(function ( cid ) {
1148
+ var c = me._conceptLib[cid];
1149
+ if ( c && c._name ) delete me._mapsByConcept[c._name];
1150
+ delete me._conceptLib[cid];
1151
+ });
1152
+ this._conceptSnapshot = null;// concept lib changed — invalidate the N6 schema snapshot cache
1153
+ return concept;
1154
+ },
1155
+ /**
1156
+ * Drain structural ops (add/patch/deleteConcept) that were queued because they were
1157
+ * issued mid-stabilize. Runs at the quiescent _loopTF boundary, so the re-eval
1158
+ * sees a settled, consistent cast-state. Each op writes/destabilizes; _loopTF's
1159
+ * re-arm then re-stabilizes (no extra kick needed). (#11.a)
1160
+ */
1161
+ _drainStructural: function () {
1162
+ var debug = this._log;
1163
+ var me = this, q = this._pendingStructural;
1164
+ this._pendingStructural = [];
1165
+ q.forEach(function ( o ) {
1166
+ try {
1167
+ var r = o.op === 'patch' ? me._doPatchConcept(o.nameOrId, o.updates)
1168
+ : o.op === 'delete' ? me._doDeleteConcept(o.nameOrId)
1169
+ : me._doAddConcept(o.parentNameOrId, o.schema);
1170
+ o.cb && o.cb(null, r);
1171
+ } catch ( e ) {
1172
+ debug.error("drainStructural %s failed: %s", o.op, e && e.message);
1173
+ o.cb && o.cb(e);
1174
+ }
1175
+ });
1176
+ },
1177
+ pushAtomicData: function ( data, revFrom, token ) {
1178
+ var debug = this._log;
1179
+ var me = this;
1180
+ debug.log("Start pushing from client %j", revFrom);
1181
+ token = isArray(token) ? token : token && [token] || [];
1182
+
1183
+ this.pushMutation(data.tpl, data.parent, true);
1184
+ token.map(( t ) => me._syncTokensList.push(t));
1185
+ // token && this.on("stabilize", function fn() {// sync cb
1186
+ // me.un("stabilize", fn);
1187
+ // token.map(( t )=>me._syncTokens[t] && me._syncTokens[t]());
1188
+ //
1189
+ // });
1190
+ },
1191
+ /**
1192
+ * Apply a mutation template — the one path that creates/updates graph objects.
1193
+ * Every touched object is destabilized so stabilization re-tests concepts on it.
1194
+ *
1195
+ * A template is an object (or array of objects) describing nodes/segments/docs.
1196
+ * Keys are facts; a few `$`-prefixed forms have meaning (the WRITE half of the
1197
+ * ref DSL — see getRef for the read half):
1198
+ * _id create a new object with a fresh id
1199
+ * $_id derive this object's id by RESOLVING a ref (e.g. "_parent" → targetId)
1200
+ * $$_id force a LITERAL id (upsert that exact object)
1201
+ * $key make `key` a REFERENCE (its value is resolved to an id)
1202
+ * $$key mark `key` a bagRef (external data id, preloaded before apply)
1203
+ * _incoming / _outgoing nest child segments under a node
1204
+ * { __push: v } race-free append to an array fact (see Entity.set)
1205
+ *
1206
+ * Pipeline:
1207
+ * 1. WALK the template (a stack), resolving `$`-refs against `refScope`/`aliases`
1208
+ * and collecting bagRefs to preload. Template-local ids are aliased to graph ids.
1209
+ * 2. PRELOAD any bagRefs (async), then in the callback:
1210
+ * 3. INSTANTIATE — nodes first, then segments/docs (so a segment's endpoints exist),
1211
+ * via Entity.update; record a revision (revTpl) for history/sync; fire the
1212
+ * onConceptApply trace if this came from a concept (applyCtx); destabilize +
1213
+ * stabilize().
1214
+ * Non-master graphs forward the template to the master (cfg.pushToMaster) and apply
1215
+ * the streamed-back result instead of mutating locally.
1216
+ *
1217
+ * @param {object|object[]} template the mutation template
1218
+ * @param {string} targetId object the template is applied to ("_parent")
1219
+ * @param {boolean} [force] bypass the master/client deferral
1220
+ * @param {*} [atomId] atomic-update id (master/client sync)
1221
+ * @param {object} [initialRefBag] preseeded bagRefs
1222
+ * @param {fn} [cb] called once applied (after bagRef preload)
1223
+ * @param {object} [applyCtx] concept-apply trace context (from Concept.applyTo)
1224
+ */
1225
+ pushMutation: function ( template, targetId, force, atomId, initialRefBag, cb, applyCtx ) {
1226
+ template = isArray(template) ? template : [template];
1227
+ var debug = this._log;// per-graph logger (captured by nested callbacks below)
1228
+ var me = this,
1229
+ cObject, cTargetObj,
1230
+ push = Array.prototype.push,
1231
+ tid, refId, revId, revNum,
1232
+ cTplObject,
1233
+ revTpl = [],
1234
+ stack = [],
1235
+ pendingObjects = [],
1236
+ pendingRefs = [],
1237
+ pendingRefMaps = [],
1238
+ pendingERefs = [],
1239
+ refs = {},
1240
+ masterToken,
1241
+ keepRev = !this.cfg.isMaster,
1242
+ originCMap = targetId && me.getEtty(targetId) && me.getEtty(targetId)._ || false,
1243
+ baseOrigin = originCMap && originCMap._keepOrigin && originCMap._origin || false,
1244
+ aliases = ["_parent"],
1245
+ refScope = {
1246
+ _parent: targetId
1247
+ },
1248
+ bagRefs = initialRefBag ? { ...initialRefBag } : {};
1249
+
1250
+ let refMap = {};
1251
+ if ( !force && !this.cfg.isMaster ) {
1252
+ debug.warn("pushing 2 master", targetId);
1253
+ // if (!me.cfg.isMaster && !me._inited){
1254
+ // this._taskFlow.wait();
1255
+ //
1256
+ // return me.on("atomicUpdate", function atomicUpdate() {
1257
+ // me.un("atomicUpdate", atomicUpdate);
1258
+ // me._inited=true;
1259
+ // me.pushMutation.apply(me, arguments);
1260
+ // me._taskFlow.release();
1261
+ // });
1262
+ // }
1263
+ if ( !this._taskFlow.running ) {
1264
+ this._taskFlow.run();
1265
+ }
1266
+ this._running = true;
1267
+ me._taskFlow.wait();
1268
+ masterToken = this.cfg.pushToMaster(
1269
+ {
1270
+ baseRev: me._rev,
1271
+ parent : targetId,
1272
+ tpl : template
1273
+ }
1274
+ );
1275
+
1276
+ this._syncTokens[masterToken] = () => {// here the server should have applied this mutation & pushed back the resulting mutations
1277
+ me._taskFlow.release();
1278
+ debug.info('Complete %s !', masterToken);
1279
+ delete me._syncTokens[masterToken];
1280
+ cb && cb({ /* should have refscope here*/ })
1281
+ };
1282
+ debug.warn('RT Push request waiting ', masterToken);
1283
+ return;
1284
+ }
1285
+
1286
+ if ( this._mutationThreadRunning ) {
1287
+ debug.warn('Delay mutation', this._mutationThread.length);
1288
+
1289
+ return this._mutationThread.push([...arguments]);
1290
+ }
1291
+ this._mutationThreadRunning = true;
1292
+
1293
+ //while ( i < max ) {
1294
+ // atoms[i]
1295
+ // && atoms[i].bagRefs
1296
+ // && atoms[i].bagRefs.length
1297
+ // && atoms[i].bagRefs.each(id => {
1298
+ // allRefs[id] = allRefs[id] || { count: 0 };
1299
+ // allRefs[id].count++;
1300
+ // });
1301
+ // i++;
1302
+ //}
1303
+
1304
+ push.apply(stack, template);
1305
+
1306
+ // if ( !keepRev ) {
1307
+ revId = atomId || shortid.generate();
1308
+ // }
1309
+
1310
+ // !keepRev &&
1311
+ revNum = this._rev;
1312
+ this._rev++;
1313
+ // !keepRev && push.bind(this._revs[this._rev].tpl, template);
1314
+
1315
+ // parse objects...
1316
+ while ( cTplObject = stack.shift() ) {
1317
+
1318
+ // create id & map innertpl ids
1319
+ refId = cTplObject.$_id && this.getRef(cTplObject.$_id, refScope,
1320
+ null,
1321
+ null,
1322
+ // required to work when referencing inner tpl from graph items
1323
+ ( id ) => {
1324
+ return (refScope[id] || this.getEtty(id)) && {
1325
+ get: ( key ) => (
1326
+ refScope[id] && refScope[id].hasOwnProperty(key)
1327
+ ? refScope[id][key]
1328
+ : this.getEtty(id) && this.getEtty(id).get(key)
1329
+ ),
1330
+ }
1331
+ });
1332
+
1333
+ if ( refId && !isString(refId) )
1334
+ refId = refId._id;
1335
+
1336
+ if ( cTplObject.$$_id ) {
1337
+ refId = cTplObject.$$_id;
1338
+ if ( cTplObject.$$_id == cTplObject._id )
1339
+ delete cTplObject._id;
1340
+ }
1341
+
1342
+ if ( !refId && cTplObject._id ) {
1343
+ if ( refs[cTplObject._id] ) {// if there was a previous tpl item with same ref
1344
+ tid = refs[cTplObject._id];
1345
+ }
1346
+ else if ( this._objById[cTplObject._id] ) // keep id if no objects use it
1347
+ tid = refs[cTplObject._id] = refId || shortid.generate();// force it if refid = $(id) is specified
1348
+ else tid = refs[cTplObject._id] = cTplObject._id;
1349
+ }
1350
+ else if ( refId && cTplObject._id ) {
1351
+ tid = refs[cTplObject._id] = refId;
1352
+ aliases.push(cTplObject._id);
1353
+ refScope[cTplObject._id] = refId;
1354
+ }
1355
+ else
1356
+ tid = refId || shortid.generate(); // if the node inherit some other node keep his id
1357
+
1358
+ // if ( keepRev && cTplObject._rev) {// update max rev
1359
+ // this._rev = Math.max(this._rev, cTplObject._rev);
1360
+ // }
1361
+
1362
+ if ( isString(refScope[tid]) ) {
1363
+
1364
+ }
1365
+
1366
+ // now create a pushable object
1367
+ cObject = refScope[tid] = refScope[tid] || { _id: tid, _rev: revNum };
1368
+ // if (!refScope[tid])
1369
+ // refScope[tid] = cObject;
1370
+
1371
+
1372
+ Object.keys(cTplObject).forEach(
1373
+ function ( c ) {
1374
+ if ( c[0] == '$' ) {// auto ref/mount
1375
+ if ( /\$(_incoming|_outgoing|\$?_id)/.test(c) ) return;
1376
+ if ( c === "$$_refMap" ) {
1377
+
1378
+ if ( me.cfg.isMaster ) {// only the master know the real ids
1379
+ pendingRefMaps.push([cTplObject, cTplObject[c], cObject]);
1380
+ }
1381
+ else {
1382
+ // clients keep the map
1383
+ // cObject._refMap = cTplObject._refMap;
1384
+ }
1385
+ return;
1386
+ }
1387
+ var key = c.substr(1);
1388
+
1389
+ if ( c[1] == '$' && isString(cTplObject[c]) ) {// bagRef
1390
+ let bagMngr = cTplObject[c] && me._isBagRefs(cTplObject[c]);
1391
+ if ( bagMngr ) {// if this is an out ref
1392
+ bagRefs[cTplObject[c]] = bagRefs[cTplObject[c]] || { count: 0 };
1393
+ bagRefs[cTplObject[c]].count++;
1394
+ cObject[c.substr(2)] = cTplObject[c];
1395
+
1396
+ // keep ref keys to track them when importing paths
1397
+ cObject.__bagRefKeys = cObject.__bagRefKeys || [];
1398
+
1399
+ !cObject.__bagRefKeys.includes(c.substr(2))
1400
+ && cObject.__bagRefKeys.push(c.substr(2))
1401
+ }
1402
+ else // if its a string that's an internal ref
1403
+ debug.error("no manager for ref %s", c, cTplObject[c]);
1404
+ }
1405
+ else if ( isString(cTplObject[c]) ) {
1406
+ pendingERefs.push([key, cTplObject[c], cObject]);
1407
+ }
1408
+ else if ( isObject(cTplObject[c]) ) {
1409
+ // a nested object value = a child object to create (Node/Segment/doc);
1410
+ // $_id derives its id from a ref, $$_id forces a literal id
1411
+ if ( cTplObject[c].$_id ) {
1412
+ cTplObject[c]._id = me.getRef(cTplObject[c].$_id, refScope);//@note : cant ref innertpl
1413
+ }
1414
+ if ( cTplObject[c].$$_id ) {
1415
+ cTplObject[c]._id = cTplObject[c].$$_id;//@note : cant ref innertpl
1416
+ }
1417
+
1418
+ cTplObject[c]._id = cTplObject[c]._id || shortid.generate();
1419
+
1420
+ pendingRefs.push([key, cTplObject[c]._id, cObject]);
1421
+ stack.push(cTplObject[c]);
1422
+ }
1423
+
1424
+ }
1425
+ else {// simple copy
1426
+ if ( /(_incoming|_outgoing|\$?_id)/.test(c) ) return;
1427
+ if ( cTplObject[c] !== undefined )
1428
+ cObject[c] = cTplObject[c];
1429
+ }
1430
+ }
1431
+ );
1432
+
1433
+ // push incomings...
1434
+ cTplObject._incoming
1435
+ && stack.push.apply(stack, cTplObject._incoming);
1436
+ cTplObject._outgoing
1437
+ && stack.push.apply(stack, cTplObject._outgoing);
1438
+ }
1439
+ this._taskFlow.wait();
1440
+ this._preloadBagRefs(
1441
+ bagRefs,
1442
+ () => {
1443
+
1444
+ pendingRefs.map(function ( ref ) {
1445
+ ref[2][ref[0]] = ref[1];// apply inner references
1446
+ });
1447
+ pendingERefs.map(( ref ) => {
1448
+ if ( refs[ref[1]] ) {
1449
+ // local alias
1450
+ ref[2][ref[0]] = refs[ref[1]];
1451
+ }
1452
+ else {
1453
+ // if ( /_currentTask/.test(ref[1]) )
1454
+ ref[2][ref[0]] = me.getRef(
1455
+ ref[1],
1456
+ refScope,
1457
+ null,
1458
+ null,
1459
+ // required to work when referencing inner tpl from graph items
1460
+ ( id ) => {
1461
+ return (refScope[id] || this.getEtty(id)) && {
1462
+ get: ( key ) => (
1463
+ refScope[id] && refScope[id].hasOwnProperty(key) ? refScope[id][key]
1464
+ : this.getEtty(id) && this.getEtty(id).get(key)
1465
+ ),
1466
+ }
1467
+ }
1468
+ )
1469
+ ;// apply outer references
1470
+ }
1471
+ });
1472
+ // build ref map...
1473
+ if ( pendingRefMaps.length ) {
1474
+ // Object.keys(refScope).forEach(
1475
+ // function ( id ) {
1476
+ //
1477
+ // if ( aliases.includes(id) ) {
1478
+ // refMap[id] = refScope[id];
1479
+ // return null;
1480
+ // }
1481
+ // refMap[id] = refScope[id]._id;
1482
+ // });
1483
+
1484
+ pendingRefMaps.map(( ref ) => {
1485
+ if ( isString(ref[1]) ) {
1486
+ debug.error(me._objById[ref[2]._id] && me._objById[ref[2]._id]._etty._._refMap);
1487
+ }
1488
+ delete ref[0].$$_refMap;
1489
+ ref[2]._refMap = ref[0]._refMap =
1490
+ isString(ref[1]) ? {
1491
+ ...(me._objById[ref[2]._id] && me._objById[ref[2]._id]._etty._._refMap || {}),
1492
+ [ref[1]]: { ...refs }
1493
+ }
1494
+ : isObject(ref[1]) ? { ...ref[1], ...refs }
1495
+ : { ...refs } // reset with/false
1496
+ });
1497
+ }
1498
+ // we still need to instantiate them..
1499
+ Object.keys(refScope).forEach(
1500
+ function ( id ) {// 1st pass : the nodes
1501
+
1502
+ if ( aliases.includes(id) ) {
1503
+ // refMap[id] = refScope[id];
1504
+ return null;
1505
+ }
1506
+ // refMap[id] = refScope[id]._id;
1507
+
1508
+
1509
+ if ( me._objById[id] ) {// if this is an existing node
1510
+ if ( me._objById[id]._etty._.Node ) {
1511
+
1512
+ // do merge with existing
1513
+ me._objById[id]._etty.update(refScope[id], me);
1514
+
1515
+ me.toggleGraphObjectState(id, "unstable");
1516
+ return me._objById[id];
1517
+ }
1518
+ else pendingObjects.push(id);
1519
+ }
1520
+ else {
1521
+ if ( refScope[id].Node ) {
1522
+ refScope[id]._origin = refScope[id]._origin || baseOrigin || targetId;
1523
+ me._objById[id] =
1524
+ refScope[id].Node && new Node(refScope[id], me);
1525
+
1526
+
1527
+ me._nodes.push(me._objById[id]);
1528
+ me._unstable.push(me._objById[id]);
1529
+ return me._objById[id];
1530
+ }
1531
+ else pendingObjects.push(id);
1532
+ }
1533
+ }
1534
+ );
1535
+ pendingObjects.map(
1536
+ function ( id ) {// 2nd pass : the segments (they will be auto linked to the nodes..)
1537
+ if ( refScope[id].targetNode == "initialTarget" )
1538
+ revTpl.push(refScope[id]);
1539
+ if ( me._objById[id] ) {// the segment/doc exist
1540
+ // do merge with existing
1541
+ me._objById[id]._etty.update(refScope[id], me);
1542
+
1543
+ me.toggleGraphObjectState(id, "unstable");
1544
+ return me._objById[id];
1545
+ }
1546
+ else {
1547
+ if ( refScope[id].Segment ) {// create the segment
1548
+
1549
+ refScope[id]._origin = refScope[id]._origin || baseOrigin || targetId;
1550
+ me._objById[id] = new Segment(refScope[id], me);
1551
+
1552
+ me._segments.push(me._objById[id]);
1553
+ me._unstable.push(me._objById[id]);
1554
+ // me.toggleGraphObjectState(id, "unstable");
1555
+ me._objById[id]._etty.updateApplicableConcepts(me);
1556
+ return me._objById[id];
1557
+ }
1558
+ else {// records/docs
1559
+ refScope[id]._origin = refScope[id]._origin || baseOrigin || targetId;
1560
+ me._objById[id] = { _etty: new Entity(refScope[id], me) };
1561
+
1562
+ me._segments.push(me._objById[id]);
1563
+ me._unstable.push(me._objById[id]);
1564
+ return me._objById[id];
1565
+ }
1566
+ }
1567
+ }
1568
+ );
1569
+ delete refScope._parent;
1570
+
1571
+
1572
+ this._revs[revNum] = {
1573
+ id : revId,
1574
+ parent: targetId,
1575
+ bagRefs,
1576
+ tpl : Object.keys(refScope).map(
1577
+ id => {
1578
+ if ( aliases.includes(id) ) {
1579
+ refMap[id] = refScope[id];
1580
+ return null;
1581
+ }
1582
+ refMap[id] = refScope[id]._id;
1583
+ let item = { ...refScope[id] };
1584
+
1585
+ item.$$_id = item._id;
1586
+ delete item._id;
1587
+ return item;
1588
+ }
1589
+ ).filter(i => !!i)
1590
+ };
1591
+ this._on.mutation
1592
+ && this._on.mutation.map(( cb ) => cb(me));
1593
+ this.cfg.onMutationApplied
1594
+ && this.cfg.onMutationApplied(this);
1595
+ // concept-apply trace: this mutation was produced by a concept apply
1596
+ // (applyCtx threaded from Concept.applyTo). Host/sync mutations have no
1597
+ // applyCtx -> no record. prompt/reply are merged from traceProvider.
1598
+ if ( applyCtx && (this.cfg.onConceptApply || (this._on && this._on.conceptApply)) ) {
1599
+ var _rec = {
1600
+ rev : revNum,
1601
+ conceptId : applyCtx.conceptId,
1602
+ conceptName: applyCtx.conceptName,
1603
+ targetId : applyCtx.targetId,
1604
+ applyId : applyCtx.applyId,// joins this trace record to the apply's logs (graph.logger.tail({applyId}))
1605
+ kind : applyCtx.kind,
1606
+ patch : this._revs[revNum] && this._revs[revNum].tpl,
1607
+ bagRefs : this._revs[revNum] && this._revs[revNum].bagRefs,
1608
+ ms : applyCtx.ms,
1609
+ why : applyCtx.why
1610
+ };
1611
+ var _k = applyCtx.conceptId + '/' + applyCtx.targetId;
1612
+ if ( this._traceByApply && this._traceByApply[_k] ) {
1613
+ _rec.prompt = this._traceByApply[_k].prompt;
1614
+ _rec.reply = this._traceByApply[_k].reply;
1615
+ delete this._traceByApply[_k];
1616
+ }
1617
+ this.cfg.onConceptApply && this.cfg.onConceptApply(_rec);
1618
+ this._on.conceptApply && this._on.conceptApply.slice(0).map(( fn ) => fn(me, _rec));
1619
+ }
1620
+ this._mutationThreadRunning = false;
1621
+ this._taskFlow.release();
1622
+ cb && cb(refScope)
1623
+ if ( this._mutationThread.length ) {
1624
+ this.pushMutation(...this._mutationThread.shift())
1625
+ }
1626
+ this.stabilize();
1627
+
1628
+ }
1629
+ );
1630
+
1631
+ return refScope;
1632
+ },
1633
+
1634
+ // -------------------------------------------------------------------------- control
1635
+
1636
+ /**
1637
+ * Launch a stabilisation on all unstable objects
1638
+ * then call cb
1639
+ * @param cb
1640
+ */
1641
+ stabilize: function ( cb ) {
1642
+ var me = this;
1643
+ cb && this.on("stabilize", function stabilize() {
1644
+ me.un("stabilize", stabilize);
1645
+ cb(arguments);
1646
+ });
1647
+ if ( !this._taskFlow.running ) {
1648
+ this._taskFlow.run();
1649
+ }
1650
+ this._running = true;
1651
+
1652
+ },
1653
+ /**
1654
+ * Call the sync method passed in the cfg (should send last atoms to the server/client)
1655
+ * @param _cb
1656
+ */
1657
+ sync: function ( _cb ) {
1658
+ var debug = this._log;
1659
+ var me = this,
1660
+ token = this.cfg.doSync
1661
+ && this.cfg.doSync(this, _cb);
1662
+ debug.log('RT Push request:', token);
1663
+
1664
+ if ( _cb && token ) {
1665
+ this._syncTokens[token] = _cb;
1666
+ }
1667
+ else _cb && _cb();
1668
+ },
1669
+ /**
1670
+ * mk all object unstable
1671
+ */
1672
+ destabilizeThemAll: function () {
1673
+ Object.keys(this._objById).map(( k ) => this.toggleGraphObjectState(k, 'unstable'), this);
1674
+ },
1675
+ /**
1676
+ * Change some object State (dirty way..)
1677
+ * @param id
1678
+ * @param state
1679
+ * @returns {boolean}
1680
+ */
1681
+ toggleGraphObjectState: function ( id, state ) {
1682
+ var i,
1683
+ out1, out2, in1,
1684
+ obj = this._objById[id];
1685
+
1686
+ if ( state == "stable" )
1687
+ out1 = this._pending,
1688
+ out2 = this._unstable,
1689
+ in1 = this._stable;
1690
+ else if ( state == "pending" )
1691
+ out1 = this._stable,
1692
+ out2 = this._unstable,
1693
+ in1 = this._pending;
1694
+ else if ( state == "unstable" )
1695
+ out1 = this._stable,
1696
+ out2 = this._pending,
1697
+ in1 = this._unstable;
1698
+ else
1699
+ return false;
1700
+
1701
+ if ( (i = out1.indexOf(obj)) != -1 )
1702
+ out1.splice(i, 1);
1703
+ else if ( (i = out2.indexOf(obj)) != -1 )
1704
+ out2.splice(i, 1);
1705
+ else
1706
+ return false;
1707
+
1708
+ in1.push(obj);
1709
+ return true;
1710
+ },
1711
+
1712
+ // -------------------------------------------------------------------------- accessors
1713
+
1714
+ /**
1715
+ * get a resultpath (paths from getPaths) and return an PathMap object
1716
+ * @param id
1717
+ * @returns {*|null}
1718
+ */
1719
+ getOpenPathOf : function ( id ) {
1720
+ return this._objById[id]
1721
+ && this._objById[id]._etty._.OpenPaths
1722
+ && new PathMap(this._objById[id]._etty._.OpenPaths, this._objById[id]._etty);
1723
+ },
1724
+ removeObj : function ( id, justClean ) {
1725
+ var obj = this._objById[id], i;
1726
+ i = this._pending.indexOf(obj);
1727
+ (i != -1) && this._pending.splice(i, 1);
1728
+ i = this._unstable.indexOf(obj);
1729
+ (i != -1) && this._unstable.splice(i, 1);
1730
+ i = this._stable.indexOf(obj);
1731
+ (i != -1) && this._stable.splice(i, 1);
1732
+ delete this._objById[id];
1733
+ !justClean && obj._etty.destroy(true);
1734
+ },
1735
+ getConcept : function ( id ) {
1736
+ return this._conceptLib[id];
1737
+ },
1738
+ getExtOpenConcepts: function ( id ) {
1739
+ if ( this._objById[id] ) {
1740
+ this._objById[id]._etty.updateApplicableConcepts();// update in case of ..
1741
+ return this._objById[id]._etty._extOpenConcepts;
1742
+ }
1743
+ return [];
1744
+ },
1745
+
1746
+ // --- kept for reference (R&D): travel-domain path-merge / TimePeriod rollup ---
1747
+ // Application-specific (Stay/Travel/childPaths/UserRecord); not part of the V1 core
1748
+ // stabilization path. Retained as a worked example of collapsing a discovered path
1749
+ // (Graph.getPaths) back into the graph as a single segment with child paths.
1750
+ pushPath: function ( path, edgeId, name, cb ) {
1751
+ var debug = this._log;
1752
+ var
1753
+ me = this,
1754
+ scope = this.getEtty(edgeId),
1755
+ // cmaps = path._pmap.maps,
1756
+ getAllPropsInPath = PathMap.prototype.getAllPropsInPath,
1757
+ tm = getAllPropsInPath(path, "TimePeriod"),
1758
+ originId = getAllPropsInPath(path, ["_id", "isTravelStart"])[0],
1759
+ travelEnds = getAllPropsInPath(path, ["_id", "isTravelEnd"]),
1760
+ targetId = travelEnds[travelEnds.length - 1],
1761
+ pathId = shortid.generate(),
1762
+ tpl = path.tpl.map(( obj ) => ({ ...obj, pathId })),
1763
+ rpath,
1764
+ originSrc = scope.getRef('originNode'),
1765
+ originTarget = scope.getRef('targetNode'),
1766
+ tId = tpl[1]._id,
1767
+ tId2 = tpl[tpl.length - 2]._id;//shortid.generate();
1768
+
1769
+ // rm origin target & origin
1770
+ // tpl.shift();
1771
+ tpl.shift();
1772
+ tpl.pop();
1773
+ // tpl.pop();
1774
+ // tpl = tpl
1775
+ tpl[0].originNode = scope._.originNode;
1776
+ tpl[tpl.length - 1].targetNode = scope._.targetNode;
1777
+ //this.pushMutationFromPath(tpl, path.descr, edgeId);
1778
+ rpath = [
1779
+ ...path.relatedTpl.map(
1780
+ ( m ) => {
1781
+ return { ...m, $_id: '$' + m._id };// use the existing one if exist
1782
+ }
1783
+ ),
1784
+ ...this.getMutationFromPath(tpl, path.descr, edgeId),
1785
+ {
1786
+ $_id : '$' + edgeId,
1787
+ OpenPaths: false,
1788
+ // Stay : null,
1789
+ // Travel : null,
1790
+ PathIgnore: !scope._.KeepInPath,// <- /!\ this will hide the segment in the debug graph and navline
1791
+ // paths
1792
+ // targetNode : originTarget,
1793
+ // originNode : null,
1794
+ childPaths: {
1795
+ ...(scope._.childPaths || {}),
1796
+ [name]: pathId
1797
+ },
1798
+ TimePeriod: null
1799
+ },
1800
+ {
1801
+ $_id : "$UserRecord",
1802
+ loadingSteps: false,
1803
+ // cFocusedEdge : tId2,
1804
+ staysCount: me.selectMapsId(["Stay"], ["VendorStep"]).length
1805
+ }
1806
+ ];
1807
+ // if (
1808
+ // scope._.targetNode == "target" ||
1809
+ // (scope.getRef("targetNode:Theoric") && edgeId !== "_root" && !scope.getRef("originNode:Theoric") )
1810
+ // ) {// root must move the initial seg
1811
+ debug.verbose("pushPath rpath", rpath)
1812
+
1813
+ this.pushMutation(
1814
+ rpath,
1815
+ edgeId
1816
+ );
1817
+ // --- kept for reference (R&D): the alternate root-segment-moving branch ---
1818
+ // } else
1819
+ // this.pushMutation(
1820
+ // [
1821
+ //
1822
+ // // {
1823
+ // // $_id : '$' + tId,
1824
+ // // _origin : '_root',
1825
+ // // },
1826
+ // // {
1827
+ // // $_id : '$' + tId2,
1828
+ // // "Undefined" : true,
1829
+ // // _origin : '_root',
1830
+ // // // fxdhfgdgfhdgdg: targetId._id
1831
+ // // },
1832
+ // {
1833
+ // $_id : '$' + edgeId,
1834
+ // OpenPaths : false,
1835
+ // // Stay : null,
1836
+ // // Travel : true,
1837
+ // // targetNode : targetId._id,
1838
+ // Undefined : false,
1839
+ // childPaths : {
1840
+ // ...(scope._.childPaths || {}),
1841
+ // [name || pathId] : pathId
1842
+ // },
1843
+ // // TimePeriod : null
1844
+ // },
1845
+ // {
1846
+ // $_id : "$UserRecord",
1847
+ // loadingSteps : false,
1848
+ // cFocusedEdge : tId2,
1849
+ // staysCount : me.selectMapsId(["Stay"], ["VendorStep"]).length
1850
+ // }
1851
+ // ],
1852
+ // edgeId
1853
+ // );
1854
+ this.stabilize(() => {
1855
+ let newPath = this.getChildPath(edgeId),
1856
+ nextTheo = newPath.reduce(( r, item ) => (item._etty._.Undefined && item._etty._._id || r), edgeId);
1857
+
1858
+
1859
+ cb && cb(nextTheo, newPath);
1860
+ });
1861
+ },
1862
+ /**
1863
+ *
1864
+ * @param origin
1865
+ */
1866
+ getChildMatching: function ( edgeId, query ) {
1867
+ let newPath = this.getChildPath(edgeId),
1868
+ fn = compileScopeQuery(query, false),
1869
+ nextTheo = newPath.filter(( item ) => fn(item._etty));
1870
+
1871
+ return nextTheo
1872
+ },
1873
+ /**
1874
+ *
1875
+ * @param origin
1876
+ */
1877
+ isTheoricChildOf: function ( cId, pId ) {
1878
+ let child = this.getEtty(cId),
1879
+ current = child;
1880
+
1881
+ while ( current ) {
1882
+ if ( current._._id == pId )
1883
+ return true;
1884
+
1885
+ current = this.getEtty(current._._origin);
1886
+ }
1887
+ return false;
1888
+ },
1889
+ /**
1890
+ *
1891
+ * @param origin
1892
+ */
1893
+ getChildPath: function ( origin, forceNoTheoric, idOnly ) {
1894
+ var debug = this._log;
1895
+ // we want paths
1896
+ var map = this._objById,
1897
+ including = isArray(forceNoTheoric) && forceNoTheoric.length ? forceNoTheoric : false,
1898
+ edge = map[origin || "_root"],
1899
+ from = origin && edge._etty.getRef('originNode') || "start",
1900
+ to = origin && edge._etty.getRef('targetNode') || "target",
1901
+ cnode = from, i, path = [from], found, sid, nid, subPath, cEdge;
1902
+ // forceNoTheoric = including;
1903
+
1904
+ // origin = origin || "_root";
1905
+ do {
1906
+ found = false;
1907
+ for ( i = 0; i < map[cnode]._outgoing.length; i++ ) {
1908
+ sid = map[cnode]._outgoing[i];
1909
+ nid = map[sid]._etty._.targetNode;
1910
+ cEdge = map[sid]._etty._;
1911
+
1912
+ if ( sid == origin && map[cnode]._outgoing.length > 1 ) continue;
1913
+
1914
+ if ( (!cEdge.PathIgnore || (cEdge.PathIgnore && cEdge.KeepInPath)) && cEdge._origin == origin ) {
1915
+ if ( path.indexOf(nid) != -1 ) debug.error("This graph have loops", path, nid);
1916
+
1917
+ if ( cEdge.Theoric && forceNoTheoric && (!including || including.includes(cEdge._id)) ) {// so get the complete child path
1918
+ subPath = this.getChildPath(sid, true, true);
1919
+ if ( subPath.length ) {
1920
+ subPath.shift();
1921
+ path.push(...subPath);
1922
+ }
1923
+ else
1924
+ path.push(sid, nid);
1925
+ }
1926
+ else
1927
+ path.push(sid, nid);
1928
+
1929
+ cnode = nid;
1930
+ found = true;
1931
+ break;
1932
+ }
1933
+ else {
1934
+ continue;
1935
+ }
1936
+
1937
+ if ( cnode === to )
1938
+ break;
1939
+ }
1940
+
1941
+ if ( !found ) {// take first
1942
+ sid = map[cnode]._outgoing[0];
1943
+
1944
+ if ( sid == origin ) sid = map[cnode]._outgoing[1];
1945
+
1946
+ if ( !sid ) {
1947
+ return [];
1948
+ }
1949
+
1950
+ path.push(sid, map[sid]._etty._.targetNode);
1951
+ cnode = map[sid]._etty._.targetNode;
1952
+ found = true;
1953
+ }
1954
+
1955
+ if ( cnode === to )
1956
+ break;
1957
+ } while ( 1 );
1958
+ // if ( origin == "_root" )
1959
+
1960
+
1961
+ return idOnly && path || path.map(( id ) => map[id]);
1962
+ },
1963
+
1964
+ /**
1965
+ * Get a all paths between fromId&toId & return them in json
1966
+ * @param fromId
1967
+ * @param toId
1968
+ * @param ignoreMissing
1969
+ * @returns {{maps: {}, paths: Array}}
1970
+ */
1971
+ getPaths: function ( fromId, toId, skip, opts ) {
1972
+ var debug = this._log;
1973
+ var map = this._objById,
1974
+ cmaps = {},
1975
+ start = this._objById[fromId],
1976
+ end = this._objById[toId],
1977
+ paths = [],
1978
+ stack = [],
1979
+ skipping = skip || [],
1980
+ // ZOOM over the abstraction stack (§9.3-§9.4): by default getPaths prefers the
1981
+ // concrete realization of a theoric (haveNoTheoric) so it returns the fully
1982
+ // EXPANDED path. `collapse` returns the abstract method-level path instead
1983
+ // (theoric hops kept, their realized bodies hidden = "carry the contract, not the
1984
+ // body"); `zoom` is the set of theoric ids to descend INTO even while collapsing
1985
+ // (open one box). Default (no opts) is unchanged & backward compatible.
1986
+ collapse = !!(opts && opts.collapse),
1987
+ zoom = (opts && opts.zoom) || [],
1988
+ related,
1989
+ haveNoTheoric,
1990
+ cpath = [fromId],
1991
+ cnode, i, newPath, sid, nid, cEdge;
1992
+
1993
+ if ( !map[fromId] )
1994
+ debug.error(this._id, "GetPath from node can't be found in the graph", fromId);
1995
+ if ( !map[toId] )
1996
+ debug.error(this._id, "GetPath from node can't be found in the graph", toId);
1997
+ if ( !map[toId] || !map[fromId] )
1998
+ return {
1999
+ maps : cmaps,
2000
+ paths: paths
2001
+ };
2002
+ cmaps[fromId] = { ...map[fromId]._etty._ };
2003
+
2004
+ do {
2005
+ cnode = cpath[cpath.length - 1];//last is node
2006
+ haveNoTheoric = map[cnode]._outgoing.reduce(
2007
+ ( p, c ) => {
2008
+ return p || !map[c]._etty._.Theoric
2009
+ }, false);// knowing if there only theoric ways
2010
+
2011
+ for ( i = 0; i < map[cnode]._outgoing.length; i++ ) {
2012
+ newPath = cpath.slice();
2013
+ sid = map[cnode]._outgoing[i];
2014
+ cEdge = map[sid]._etty._;
2015
+ nid = cEdge.targetNode;// node
2016
+
2017
+ if ( collapse ) {
2018
+ // abstraction-stack traversal: keep theoric METHOD hops, hide their
2019
+ // realized bodies — unless a method is explicitly being ZOOMED into.
2020
+ if ( cEdge.Theoric ) {
2021
+ if ( zoom.includes(sid) ) continue;// zoom in: skip the hop, take the body
2022
+ }
2023
+ else if ( cEdge._origin && map[cEdge._origin]
2024
+ && map[cEdge._origin]._etty._.Theoric
2025
+ && !zoom.includes(cEdge._origin) )
2026
+ continue;// a body step of a collapsed method -> hidden
2027
+ }
2028
+ // default: ignore theoric if a concrete realization is available
2029
+ else if ( haveNoTheoric && cEdge.Theoric )
2030
+ continue;
2031
+
2032
+ // ignore theoric if possible
2033
+ if ( skipping.includes(sid) )
2034
+ continue;
2035
+
2036
+ if ( (related = map[sid]._etty._.pathDescriptor) && map[related] ) {
2037
+ do {
2038
+ cmaps[related] = { ...map[related]._etty._ };
2039
+ } while ( related = map[related] && map[related]._etty._.parentPathDescriptor );// assume broken
2040
+ // refs are good in
2041
+ // other graphs
2042
+ }
2043
+
2044
+ if ( (related = map[sid]._etty._._origin) && map[related] ) {// add _origin ( the theoric which has generated this segment )
2045
+
2046
+ do {
2047
+ // if (map[related] && map[related]._etty._.thisOne)
2048
+
2049
+ if ( skipping.includes(related) )
2050
+ break;
2051
+
2052
+ cmaps[related] = { ...map[related]._etty._ };
2053
+ related = map[related] && map[related]._etty._._origin
2054
+
2055
+ } while ( related && map[related] && !cmaps[related] );
2056
+ }
2057
+
2058
+ newPath.push(sid, nid);
2059
+
2060
+ if ( !cmaps[sid] )
2061
+ cmaps[sid] = { ...map[sid]._etty._ };
2062
+ // if ( !cmaps[nid] && !map[nid]){
2063
+ // nid,
2064
+ // map[sid]._etty._
2065
+ // )
2066
+ // }
2067
+ if ( !cmaps[nid] )
2068
+ cmaps[nid] = { ...map[nid]._etty._ };
2069
+
2070
+ if ( nid === toId )
2071
+ paths.push(newPath);
2072
+ else
2073
+ stack.push(newPath);
2074
+ }
2075
+
2076
+ if ( !stack.length ) break;
2077
+ cpath = stack.shift();
2078
+ } while ( 1 );
2079
+
2080
+ return {
2081
+ maps : cmaps,
2082
+ paths: paths
2083
+ };
2084
+ },
2085
+
2086
+ /**
2087
+ * We need to know if a vendor record is mounted from some point,
2088
+ * or we'll not have any method to know if a vendorRecord is still available
2089
+ *
2090
+ * @param to
2091
+ * @param manager // should contain getFormByRecordId, checkRecordValidityById, ...
2092
+ */
2093
+ registerVendorRecordByKey: function ( to, manager ) {
2094
+ },
2095
+ /**
2096
+ * Get an object by his id
2097
+ * @param id
2098
+ * @returns {*}
2099
+ */
2100
+ getObjById: function ( id ) {
2101
+ return this._objById[id];
2102
+ },
2103
+
2104
+ /**
2105
+ * Get the concept map by his id
2106
+ * @param id
2107
+ * @returns {*}
2108
+ */
2109
+ getEtty: function ( id ) {
2110
+ return this._objById[id] && this._objById[id]._etty;
2111
+ },
2112
+ /**
2113
+ * Select all node that match the query
2114
+ * @param _with Array
2115
+ * @param _without Array
2116
+ * @returns {array|*|Array}
2117
+ */
2118
+ queryMaps: function ( query ) {
2119
+
2120
+ var me = this,
2121
+ maps = this._objById,
2122
+ fn = compileScopeQuery(query, true)
2123
+
2124
+
2125
+ ;
2126
+ return Object.keys(maps).map(( k ) => maps[k]._etty).filter(fn);
2127
+
2128
+ },
2129
+ /**
2130
+ * Select all node with _with prop but whithout _whithout props
2131
+ * @param _with Array
2132
+ * @param _without Array
2133
+ * @returns {array|*|Array}
2134
+ */
2135
+ selectMaps: function ( _with, _without ) {
2136
+ var me = this;
2137
+ return this.selectMapsId(_with, _without).map(( v ) => me._objById[v] && me._objById[v]._etty);
2138
+
2139
+ },
2140
+
2141
+ /**
2142
+ * Select all node id with _with prop but whithout _whithout props
2143
+ * @param _with Array
2144
+ * @param _without Array
2145
+ * @returns {array|*|Array}
2146
+ */
2147
+ selectMapsId: function ( _with, _without ) {
2148
+ var i = 0, me = this, maps = this._mapsByConcept,
2149
+ _with = isArray(_with) ? _with : [_with],
2150
+ _have,
2151
+ _without = isArray(_without) ? _without : [_without];
2152
+
2153
+
2154
+ _with.map(( v ) => maps[v] && (_have = intersect(_have || maps[v], maps[v])));
2155
+ _have && _without.map(( v ) => maps[v] && (_have = arrayDiffer(_have, maps[v])));
2156
+
2157
+
2158
+ return _have || [];
2159
+
2160
+ },
2161
+
2162
+ // -------------------------------------------------------------------------- events
2163
+
2164
+ on: function ( evt, cb ) {
2165
+ if ( !isFunction(cb) ) throw 'wtf';
2166
+
2167
+ this._on[evt] = this._on[evt] || [];
2168
+ this._on[evt].push(cb);
2169
+
2170
+ },
2171
+ un: function ( evt, cb ) {
2172
+ //this._on[evt] = this._on[evt]||[];
2173
+ if ( !this._on[evt] ) return;
2174
+ var i = this._on[evt].indexOf(cb);
2175
+ this._on[evt].splice(i, 1);
2176
+ },
2177
+ /**
2178
+ * Provider trace hook: lets a provider report the prompt/reply (or any extra
2179
+ * payload) it produced for the current apply, WITHOUT coupling the engine to
2180
+ * providers. No-op unless a concept-apply trace sink is configured. The payload
2181
+ * is merged into the trace record emitted when the provider's mutation lands.
2182
+ * Keyed by concept+target so concurrent async providers don't collide.
2183
+ * @param concept the concept (provider's 2nd arg)
2184
+ * @param scope the scope Entity (provider's 3rd arg)
2185
+ * @param payload e.g. { prompt, reply }
2186
+ */
2187
+ traceProvider: function ( concept, scope, payload ) {
2188
+ if ( !this.cfg.onConceptApply && !(this._on && this._on.conceptApply) ) return;
2189
+ (this._traceByApply = this._traceByApply || {})[concept._id + '/' + scope._._id] = payload;
2190
+ },
2191
+ /**
2192
+ * Called once stabilized
2193
+ * @private
2194
+ */
2195
+ _applyStabilized: function () {
2196
+ this._log.info('graph settled (fixpoint) at rev %s', this._rev);
2197
+ var me = this;
2198
+ this._stabilized = true;
2199
+ this._stabilizing = false;// pass complete — host ops (incl. those issued from onStabilize) apply immediately again
2200
+ this._applyCount = {};// healthy settle ends the episode — clear the per-(target/concept) apply tally (#11.c.1)
2201
+ this._lastSettledRev = this.getCurrentRevision();// last clean checkpoint (has a snapshot) for a reactive supervisor's rollback (#11.c.4)
2202
+ me._running = false;
2203
+ // me._rev++;// graph inst revision
2204
+ this._captureSnapshot();// checkpoint this coherent state so rollbackTo() can restore it
2205
+ this._on.stabilize
2206
+ && this._on.stabilize.slice(0).map(( cb ) => cb(me, me._syncTokensList));
2207
+ this.cfg.onStabilize
2208
+ && this.cfg.onStabilize(this, me._syncTokensList);
2209
+ me._syncTokensList = [];
2210
+ },
2211
+ history_push : function ( mutation, targetId, isStep ) {
2212
+ },
2213
+ history_goto : function ( to ) {
2214
+ return this.rollbackTo(to);
2215
+ },
2216
+ // -------------------------------------------------------------------------- rollback
2217
+ /**
2218
+ * Checkpoint the current stabilized (coherent) state so rollbackTo() can
2219
+ * restore it later. Keyed by the current revision; no-op if already captured.
2220
+ * Snapshots are full serialized states (delta replay is left for later).
2221
+ * @private
2222
+ */
2223
+ _captureSnapshot: function () {
2224
+ this._snapshots = this._snapshots || {};
2225
+ var rev = this.getCurrentRevision();
2226
+ if ( !this._snapshots[rev] ) {
2227
+ var snap = this.serialize();// { lastRev, graph } — the FACT state
2228
+ // N6 (#11.c.2): also snapshot the FULL live concept schema tree, so rollbackTo()
2229
+ // restores the rules (runtime add/patchConcept), not just the facts. Cached and
2230
+ // invalidated on each concept-lib edit, so unchanged libs aren't re-serialized.
2231
+ snap.concepts = this._conceptSnapshot || (this._conceptSnapshot = this._serializeConceptTree());
2232
+ this._snapshots[rev] = snap;
2233
+
2234
+ // Bounded revision log (opt-in, R11.6): in a long-running/live regime _snapshots +
2235
+ // _revs grow unbounded (each settle/mutation appends). `cfg.maxRevisions` keeps a
2236
+ // retained window [rev-cap, rev]; a rollbackTo() BEYOND the window hits the existing
2237
+ // "no snapshot for revision" throw, so the only effect is a bounded history (the
2238
+ // retained snapshots still restore exactly). Default unset = unbounded (no change).
2239
+ var cap = this.cfg && this.cfg.maxRevisions;
2240
+ if ( cap > 0 && rev - cap > 0 ) {
2241
+ var floor = rev - cap;
2242
+ for ( var k in this._snapshots ) if ( Number(k) < floor ) delete this._snapshots[k];
2243
+ for ( var r = 0; r < floor; r++ ) if ( this._revs[r] != null ) delete this._revs[r];
2244
+ }
2245
+ }
2246
+ },
2247
+ /**
2248
+ * Serialize the LIVE concept tree (reflecting runtime add/patchConcept) into a nested
2249
+ * record `new Concept(...)` can rebuild — walking `_openConcepts` (so adds are caught)
2250
+ * and reading each concept's current `_schema` (so patches are caught; the parent's
2251
+ * `_schema.childConcepts` is NOT authoritative after a patch). JSON-cloned (schemas are
2252
+ * JSON-safe: providers are string refs, no functions). (#11.c.2 / N6)
2253
+ */
2254
+ _serializeConceptTree: function () {
2255
+ function ser( c ) {
2256
+ var out = {}, s = c._schema || {};
2257
+ Object.keys(s).forEach(function ( k ) { if ( k !== 'childConcepts' ) out[k] = s[k]; });
2258
+ if ( c._openConcepts ) {
2259
+ var ids = Object.keys(c._openConcepts);
2260
+ if ( ids.length ) {
2261
+ out.childConcepts = {};
2262
+ ids.forEach(function ( id ) { out.childConcepts[id] = ser(c._openConcepts[id]); });
2263
+ }
2264
+ }
2265
+ return out;
2266
+ }
2267
+ return JSON.parse(JSON.stringify(ser(this._rootConcept)));
2268
+ },
2269
+ /**
2270
+ * Public: the LIVE concept tree as a serializable record (reflecting runtime add/patchConcept).
2271
+ * The inverse of the `conceptMap` you boot with — feed it to corpus-pack#packCorpus for a
2272
+ * portable `.sgc` bundle, or to load.js#exportConceptsToDir to write the on-disk JSONC layout.
2273
+ * @returns {{childConcepts:object}} a tree `new Graph(..., { <set>: tree })` can reload.
2274
+ */
2275
+ exportConcepts: function () {
2276
+ return this._serializeConceptTree();
2277
+ },
2278
+ /**
2279
+ * Rebuild the concept library from a snapshotted schema tree (deep-cloned so later
2280
+ * edits don't mutate the stored snapshot). Used by rollbackTo. (#11.c.2 / N6)
2281
+ */
2282
+ _restoreConceptTree: function ( tree ) {
2283
+ this._conceptLib = {};
2284
+ this._mapsByConcept = this._mapsByConcept || {};
2285
+ this._rootConcept = new Concept(JSON.parse(JSON.stringify(tree)), this);
2286
+ this._conceptSnapshot = null;// the live lib changed — force a fresh capture next settle
2287
+ },
2288
+ /**
2289
+ * @returns {number[]} revisions available to rollbackTo(), ascending
2290
+ */
2291
+ getRevisions: function () {
2292
+ return Object.keys(this._snapshots || {}).map(Number).sort(( a, b ) => a - b);
2293
+ },
2294
+ /**
2295
+ * The serialized snapshot captured at `revisionNumber` (as serialize() yields:
2296
+ * `{ lastRev, graph: "<json>" }`), or null if no snapshot was captured for it.
2297
+ * @param revisionNumber
2298
+ */
2299
+ getSnapshot: function ( revisionNumber ) {
2300
+ return (this._snapshots && this._snapshots[revisionNumber]) || null;
2301
+ },
2302
+ /**
2303
+ * Parse a snapshot into a { objectId -> facts } map (the serialized
2304
+ * conceptMaps, keyed by _id). `_rev` is dropped (volatile per-object marker).
2305
+ * @private
2306
+ */
2307
+ _snapshotFacts: function ( snap ) {
2308
+ var maps = (JSON.parse(snap.graph).conceptMaps) || [], by = {};
2309
+ maps.forEach(function ( m ) {
2310
+ var f = {};
2311
+ Object.keys(m).forEach(function ( k ) { if ( k !== '_rev' ) f[k] = m[k]; });
2312
+ by[m._id] = f;
2313
+ });
2314
+ return by;
2315
+ },
2316
+ /**
2317
+ * Diff two captured revisions: what revision `b` added / removed / changed
2318
+ * versus revision `a`. The inspection layer of "Git for reasoning".
2319
+ *
2320
+ * @param a revision (from getRevisions())
2321
+ * @param b revision (from getRevisions())
2322
+ * @returns {{added:Object, removed:Object, changed:Object}}
2323
+ * added/removed: { id -> facts }; changed: { id -> { key -> [beforeVal, afterVal] } }
2324
+ * @throws if either revision has no snapshot
2325
+ */
2326
+ diffRevisions: function ( a, b ) {
2327
+ var snapA = this.getSnapshot(a), snapB = this.getSnapshot(b);
2328
+ if ( !snapA ) throw new Error("diffRevisions: no snapshot for revision " + a);
2329
+ if ( !snapB ) throw new Error("diffRevisions: no snapshot for revision " + b);
2330
+
2331
+ var fa = this._snapshotFacts(snapA), fb = this._snapshotFacts(snapB),
2332
+ added = {}, removed = {}, changed = {};
2333
+
2334
+ Object.keys(fb).forEach(function ( id ) { if ( !fa[id] ) added[id] = fb[id]; });
2335
+ Object.keys(fa).forEach(function ( id ) { if ( !fb[id] ) removed[id] = fa[id]; });
2336
+ Object.keys(fa).forEach(function ( id ) {
2337
+ if ( !fb[id] ) return;
2338
+ var keys = {}, d = {};
2339
+ Object.keys(fa[id]).forEach(function ( k ) { keys[k] = 1; });
2340
+ Object.keys(fb[id]).forEach(function ( k ) { keys[k] = 1; });
2341
+ Object.keys(keys).forEach(function ( k ) {
2342
+ if ( JSON.stringify(fa[id][k]) !== JSON.stringify(fb[id][k]) )
2343
+ d[k] = [fa[id][k], fb[id][k]];
2344
+ });
2345
+ if ( Object.keys(d).length ) changed[id] = d;
2346
+ });
2347
+ return { added: added, removed: removed, changed: changed };
2348
+ },
2349
+ /**
2350
+ * Roll the whole graph back to a previously stabilized revision: re-mount that
2351
+ * snapshot and re-stabilize (re-fires onStabilize). Snapshots strictly after
2352
+ * `revisionNumber` are discarded — this is a linear undo, the restored timeline
2353
+ * replaces the abandoned one.
2354
+ *
2355
+ * @param revisionNumber a revision from getRevisions() / getCurrentRevision()
2356
+ * @returns {number} the restored revision
2357
+ * @throws {Error} if no snapshot exists for that revision
2358
+ */
2359
+ rollbackTo: function ( revisionNumber ) {
2360
+ var snap = this._snapshots && this._snapshots[revisionNumber];
2361
+ if ( !snap )
2362
+ throw new Error("rollbackTo: no snapshot for revision " + revisionNumber +
2363
+ " (available: " + this.getRevisions().join(', ') + ")");
2364
+
2365
+ // N6 (#11.c.2): restore the concept LIBRARY first (before mount re-evaluates objects
2366
+ // against it), so a runtime add/patchConcept made after this rev is undone too — else
2367
+ // a surviving concept re-casts and the rolled-back edit "resurrects".
2368
+ if ( snap.concepts ) this._restoreConceptTree(snap.concepts);
2369
+
2370
+ // Issued mid-stabilize (e.g. from a supervisor concept's provider)? Defer to the
2371
+ // quiescent _loopTF boundary — rollbackTo re-mounts, which must not happen mid-pass
2372
+ // (#11.c.4). Multiple requests collapse to the EARLIEST rev (roll back furthest).
2373
+ if ( this._stabilizing ) {
2374
+ this._pendingRollback = (this._pendingRollback == null)
2375
+ ? revisionNumber : Math.min(this._pendingRollback, revisionNumber);
2376
+ return revisionNumber;
2377
+ }
2378
+
2379
+ this._doRollback(revisionNumber);
2380
+ this._taskFlow.run();// re-stabilize -> re-fires _applyStabilized / onStabilize
2381
+ this._running = true;
2382
+ return revisionNumber;
2383
+ },
2384
+ /**
2385
+ * The re-mount body of rollbackTo WITHOUT the stabilize kick — restore the concept
2386
+ * lib (N6) then the facts, and drop the abandoned future. Used by the public method
2387
+ * (host path, then kicks) and by the mid-stabilize drain (`_loopTF`, no kick — the
2388
+ * re-arm re-stabilizes since mount leaves objects unstable). (#11.c.4)
2389
+ */
2390
+ _doRollback: function ( revisionNumber ) {
2391
+ var me = this,
2392
+ snap = this._snapshots && this._snapshots[revisionNumber];
2393
+ if ( !snap ) return;
2394
+ if ( snap.concepts ) this._restoreConceptTree(snap.concepts);// N6: rules first
2395
+ this.mount(JSON.parse(snap.graph));// rebuild _objById at that rev, mark unstable, set _rev
2396
+ Object.keys(this._snapshots).forEach(function ( r ) {
2397
+ if ( Number(r) > revisionNumber ) delete me._snapshots[r];// drop the abandoned future
2398
+ });
2399
+ this._stabilized = false;
2400
+ },
2401
+ // -------------------------------------------------------------------------- fork / merge
2402
+ /**
2403
+ * Fork an independent child Graph (a sub-agent sandbox) to develop a path /
2404
+ * sub-problem on its own, optionally with a different concept set (= different
2405
+ * capabilities). Reuses this graph's concept library unless `conf.conceptMap`
2406
+ * overrides it. Thin wrapper over `new Graph(...)` — no new core machinery.
2407
+ *
2408
+ * @param seed serialized graph / {conceptMaps|nodes|segments} seeding the child;
2409
+ * omitted -> forks this graph's current snapshot (serialize()).
2410
+ * @param conf cfg overrides merged onto this graph's cfg. Extras:
2411
+ * `conceptMap` (override library); `reintegrateInto` (targetId) +
2412
+ * `project` -> auto-merge the child's result back here on its stabilize.
2413
+ * @returns {Graph} the child graph
2414
+ */
2415
+ fork: function ( seed, conf ) {
2416
+ var me = this;
2417
+ conf = conf || {};
2418
+ var reintegrateInto = conf.reintegrateInto,
2419
+ project = conf.project,
2420
+ userOnStabilize = conf.onStabilize,
2421
+ conceptMap = conf.conceptMap || this._conceptMap;
2422
+
2423
+ var childConf = { ...this.cfg, isMaster: true, autoMount: true, ...conf };
2424
+ delete childConf.reintegrateInto;
2425
+ delete childConf.project;
2426
+ delete childConf.conceptMap;
2427
+
2428
+ if ( reintegrateInto != null ) {
2429
+ childConf.onStabilize = function ( child, tokens ) {
2430
+ userOnStabilize && userOnStabilize(child, tokens);
2431
+ if ( !child._merged ) {
2432
+ child._merged = true;
2433
+ me.merge(child, reintegrateInto, project);
2434
+ }
2435
+ };
2436
+ }
2437
+
2438
+ var record = seed || JSON.parse(this.serialize().graph);
2439
+ var child = new this.static(record, childConf, conceptMap);
2440
+ (this._forks = this._forks || []).push(child);
2441
+ return child;
2442
+ },
2443
+ /**
2444
+ * Reintegrate a forked child's result into this graph: apply project(child) (a
2445
+ * mutation template) onto `targetId`, then destroy the child.
2446
+ *
2447
+ * @param child a Graph returned by fork()
2448
+ * @param targetId the object in THIS graph to merge the result onto
2449
+ * @param project (child) -> mutation template; default attaches child.serialize()
2450
+ * @returns {Graph} this
2451
+ */
2452
+ merge: function ( child, targetId, project ) {
2453
+ var tpl = project
2454
+ ? project(child)
2455
+ : { $$_id: targetId, forkResult: JSON.parse(child.serialize().graph) };
2456
+ if ( tpl ) this.pushMutation(tpl, targetId, true);
2457
+
2458
+ var i = this._forks ? this._forks.indexOf(child) : -1;
2459
+ if ( i !== -1 ) this._forks.splice(i, 1);
2460
+ child && !child._dead && child.destroy && child.destroy();
2461
+ return this;
2462
+ },
2463
+ /**
2464
+ * clean & unref
2465
+ */
2466
+ destroy: function () {
2467
+ this._taskFlow.kill();
2468
+ var me = this;
2469
+ this._on.destroy
2470
+ && this._on.destroy.slice(0).map(( cb ) => cb(me));
2471
+ Object.keys(this._objById).map(
2472
+ ( k ) => me._objById[k].destroy ? me._objById[k].destroy() : me._objById[k]._etty.destroy());
2473
+ this._freeNodes = this._nodes = this._objById = me._conceptLib = me._syncTokens =
2474
+ me._syncTokensList = this._segments =
2475
+ this._rootConcept = this._mapsByConcept = this._on =
2476
+ this._history = this._unstable = this._pending = this._stable = this._objById = this._pendingMutationsById = null;
2477
+ this._dead = true;
2478
+ }
2479
+ };
2480
+
2481
+ // The debugger/host logging interface: graph.logger.addSink(fn) / removeSink /
2482
+ // tail(n, {concept|target|applyId|level}) / records / setLevel(name). Logs live
2483
+ // here (bounded ring buffer + sinks), never as facts on the graph objects.
2484
+ Object.defineProperty(Graph.prototype, 'logger', { get: function () { return this._log; } });
2485
+
2486
+ module.exports = Graph;