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.
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +661 -0
- package/README.md +245 -0
- package/bin/sg +20 -0
- package/concepts/README.md +23 -0
- package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
- package/concepts/_substrate/Claim/Confidence.json +10 -0
- package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
- package/concepts/_substrate/Claim/Freshness.json +5 -0
- package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
- package/concepts/_substrate/Claim/Verification.json +6 -0
- package/concepts/_substrate/Claim.json +4 -0
- package/concepts/_substrate/Frontier/InBeam.json +6 -0
- package/concepts/_substrate/Frontier/Stuck.json +6 -0
- package/concepts/_substrate/Frontier.json +5 -0
- package/concepts/_substrate/Intake/ToTask.json +22 -0
- package/concepts/_substrate/Intake/Typed.json +10 -0
- package/concepts/_substrate/Intake.json +27 -0
- package/concepts/_substrate/Task/Answer.json +12 -0
- package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
- package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
- package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
- package/concepts/_substrate/Task/Complexity.json +6 -0
- package/concepts/_substrate/Task/EvalComplexity.json +13 -0
- package/concepts/_substrate/Task/ReportUp.json +6 -0
- package/concepts/_substrate/Task/Rollup.json +10 -0
- package/concepts/_substrate/Task.json +5 -0
- package/concepts/clinical/Diagnosis/Medication.json +6 -0
- package/concepts/clinical/Diagnosis.json +15 -0
- package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
- package/concepts/clinical/Observation/LabValue.json +5 -0
- package/concepts/clinical/Observation.json +6 -0
- package/concepts/common/Document/pathBasket.json +6 -0
- package/concepts/common/Document.json +3 -0
- package/concepts/common/Edge/Distance.json +13 -0
- package/concepts/common/Edge/Stay/LongStay.json +8 -0
- package/concepts/common/Edge/Stay.json +8 -0
- package/concepts/common/Edge/Travel/LongTravel.json +16 -0
- package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
- package/concepts/common/Edge/Travel/targetNode.json +7 -0
- package/concepts/common/Edge/Travel.json +7 -0
- package/concepts/common/Edge.json +3 -0
- package/concepts/common/Vertice.json +3 -0
- package/concepts/supply/Fulfillment.json +10 -0
- package/concepts/supply/Inventory/Reorder.json +7 -0
- package/concepts/supply/Inventory.json +5 -0
- package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
- package/concepts/supply/Procurement.json +8 -0
- package/concepts/supply/Transport.json +5 -0
- package/doc/API.md +556 -0
- package/doc/CAPABILITIES.md +373 -0
- package/doc/MODELISATION.md +511 -0
- package/doc/architecture.md +267 -0
- package/doc/concept-as-graph.md +379 -0
- package/doc/concept-learning.md +168 -0
- package/doc/creative-loop-map.md +202 -0
- package/doc/original-2016-doc.md +148 -0
- package/doc/usage.md +501 -0
- package/lib/authoring/abstract.js +401 -0
- package/lib/authoring/abstraction.js +190 -0
- package/lib/authoring/adapt.js +393 -0
- package/lib/authoring/ancestry.js +216 -0
- package/lib/authoring/author.js +147 -0
- package/lib/authoring/bounded-merge.js +57 -0
- package/lib/authoring/canon.js +158 -0
- package/lib/authoring/clock.js +123 -0
- package/lib/authoring/combinator.js +92 -0
- package/lib/authoring/compete.js +76 -0
- package/lib/authoring/compose-hotspot.js +314 -0
- package/lib/authoring/compress.js +279 -0
- package/lib/authoring/concept-net.js +254 -0
- package/lib/authoring/concepts.js +66 -0
- package/lib/authoring/context-project.js +277 -0
- package/lib/authoring/contract.js +476 -0
- package/lib/authoring/corpus-pack.js +125 -0
- package/lib/authoring/cost-probe.js +86 -0
- package/lib/authoring/crystallize.js +555 -0
- package/lib/authoring/dag-decompose.js +256 -0
- package/lib/authoring/dataset-adapter.js +253 -0
- package/lib/authoring/debug-provider.js +92 -0
- package/lib/authoring/decompose.js +343 -0
- package/lib/authoring/emittability.js +236 -0
- package/lib/authoring/equilibrium.js +160 -0
- package/lib/authoring/extract.js +153 -0
- package/lib/authoring/forest.js +55 -0
- package/lib/authoring/forge-fallback.js +116 -0
- package/lib/authoring/givens.js +121 -0
- package/lib/authoring/glossary.js +177 -0
- package/lib/authoring/grammar-graph.js +191 -0
- package/lib/authoring/granularity.js +80 -0
- package/lib/authoring/graph-net.js +108 -0
- package/lib/authoring/ground.js +148 -0
- package/lib/authoring/higher-order.js +85 -0
- package/lib/authoring/hotspot.js +132 -0
- package/lib/authoring/hysteresis.js +85 -0
- package/lib/authoring/index.js +106 -0
- package/lib/authoring/lattice-morphism.js +61 -0
- package/lib/authoring/lattice-pack.js +148 -0
- package/lib/authoring/leaf-io.js +77 -0
- package/lib/authoring/library.js +156 -0
- package/lib/authoring/lifecycle.js +92 -0
- package/lib/authoring/loop.js +193 -0
- package/lib/authoring/master-loop.js +132 -0
- package/lib/authoring/mdl.js +209 -0
- package/lib/authoring/memo-stability.js +122 -0
- package/lib/authoring/method-explorer.js +138 -0
- package/lib/authoring/method-pack.js +181 -0
- package/lib/authoring/method.js +202 -0
- package/lib/authoring/mine.js +510 -0
- package/lib/authoring/mount.js +119 -0
- package/lib/authoring/negotiate.js +100 -0
- package/lib/authoring/parametric.js +139 -0
- package/lib/authoring/reaggregate.js +75 -0
- package/lib/authoring/rebalance.js +157 -0
- package/lib/authoring/recall.js +135 -0
- package/lib/authoring/registry.js +350 -0
- package/lib/authoring/relearn.js +147 -0
- package/lib/authoring/retention.js +89 -0
- package/lib/authoring/segment-proxy.js +114 -0
- package/lib/authoring/serve-leaf.js +64 -0
- package/lib/authoring/slot-aware-serve.js +77 -0
- package/lib/authoring/sound-invoke.js +59 -0
- package/lib/authoring/split-serve.js +74 -0
- package/lib/authoring/ste.js +239 -0
- package/lib/authoring/stock.js +101 -0
- package/lib/authoring/store.js +94 -0
- package/lib/authoring/supervise.js +138 -0
- package/lib/authoring/support.js +131 -0
- package/lib/authoring/task-mirror.js +51 -0
- package/lib/authoring/typed-loop.js +160 -0
- package/lib/authoring/validate.js +569 -0
- package/lib/authoring/widen.js +88 -0
- package/lib/combos/appliance.js +189 -0
- package/lib/combos/critique.js +377 -0
- package/lib/combos/defaults.js +86 -0
- package/lib/combos/durable-runner.js +115 -0
- package/lib/combos/forge.js +209 -0
- package/lib/combos/index.js +86 -0
- package/lib/combos/learning-library.js +253 -0
- package/lib/combos/mixture-serve.js +193 -0
- package/lib/combos/plan-loop.js +192 -0
- package/lib/combos/proxy-cache.js +272 -0
- package/lib/combos/self-mod.js +94 -0
- package/lib/durable/audit.js +102 -0
- package/lib/durable/checkpoint-store.js +550 -0
- package/lib/durable/fold.js +62 -0
- package/lib/durable/interpreter.js +242 -0
- package/lib/durable/xlate.js +185 -0
- package/lib/graph/Graph.js +2486 -0
- package/lib/graph/expr.js +307 -0
- package/lib/graph/index.js +26 -0
- package/lib/graph/log.js +134 -0
- package/lib/graph/objects/Concept.js +382 -0
- package/lib/graph/objects/Entity.js +503 -0
- package/lib/graph/objects/Node.js +57 -0
- package/lib/graph/objects/PathMap.js +557 -0
- package/lib/graph/objects/Segment.js +100 -0
- package/lib/graph/tasks/stabilize.js +59 -0
- package/lib/graph/tasks/taskflow.js +247 -0
- package/lib/index.js +122 -0
- package/lib/load.js +132 -0
- package/lib/providers/backends.js +117 -0
- package/lib/providers/borderline.js +129 -0
- package/lib/providers/cache.js +226 -0
- package/lib/providers/canonicalize.js +198 -0
- package/lib/providers/constat.js +107 -0
- package/lib/providers/geo.js +53 -0
- package/lib/providers/index.js +116 -0
- package/lib/providers/intake.js +263 -0
- package/lib/providers/llm-local.js +88 -0
- package/lib/providers/llm.js +236 -0
- package/lib/providers/local-host.js +197 -0
- package/lib/providers/merge-consistency.js +162 -0
- package/lib/providers/nogood.js +149 -0
- package/lib/providers/reason-loop.js +223 -0
- package/lib/providers/semiring.js +285 -0
- package/lib/providers/solver-fork.js +163 -0
- package/lib/providers/stats.js +228 -0
- package/lib/providers/verify.js +120 -0
- package/lib/runtime/index.js +135 -0
- package/lib/runtime/invoke-pool.js +92 -0
- package/lib/runtime/protocol.js +210 -0
- package/lib/runtime/transport-socket.js +77 -0
- package/lib/runtime/worker-entry.js +31 -0
- package/lib/sg/cli.js +601 -0
- package/lib/sg/log-sinks.js +214 -0
- package/lib/sg/mcp.js +395 -0
- package/lib/sg/proxy-run.js +66 -0
- package/lib/sg/serve-demo.js +185 -0
- package/lib/sg/serve.js +205 -0
- package/lib/sg/trace.js +118 -0
- package/lib/studio/protocol.js +45 -0
- package/lib/studio/public/app.js +310 -0
- package/lib/studio/public/components/ConceptEditor.js +43 -0
- package/lib/studio/public/components/ConceptTree.js +43 -0
- package/lib/studio/public/components/CorpusPanel.js +68 -0
- package/lib/studio/public/components/ForkTree.js +36 -0
- package/lib/studio/public/components/GrammarGraph.js +99 -0
- package/lib/studio/public/components/GraphCanvas.js +156 -0
- package/lib/studio/public/components/Inspector.js +48 -0
- package/lib/studio/public/components/LearningPanel.js +85 -0
- package/lib/studio/public/components/PromptConsole.js +46 -0
- package/lib/studio/public/components/ProviderTrace.js +39 -0
- package/lib/studio/public/components/SessionSplit.js +47 -0
- package/lib/studio/public/components/TilingOverlay.js +40 -0
- package/lib/studio/public/components/Timeline.js +57 -0
- package/lib/studio/public/index.html +26 -0
- package/lib/studio/public/styles.css +185 -0
- package/lib/studio/public/ws.js +58 -0
- package/lib/studio/server.js +130 -0
- package/lib/studio/session.js +397 -0
- package/lib/studio/studio.js +149 -0
- package/package.json +42 -0
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* L3 learned-NEGATIVE policy — a nogood / sound-skip ("this trial is a dead end for this
|
|
21
|
+
* context") expressed entirely with EXISTING engine machinery (doc/WIP/HANDOFF.md §7 Tier 2;
|
|
22
|
+
* experiment B). Zero core change, and provably fixpoint-preserving: it removes only the
|
|
23
|
+
* useless cast of a provably-dead trial and never changes a surviving useful conclusion
|
|
24
|
+
* (B verified the surviving set bit-for-bit identical with and without the policy).
|
|
25
|
+
*
|
|
26
|
+
* It generalizes the engine's own "`divergent` fact = non-cast condition" from per-(object,
|
|
27
|
+
* concept) to a LEARNED per-(contextKey, trial) policy:
|
|
28
|
+
* - on a dead-end trial result, the trial's provider {__push}es a `{ctxKey, trial}` nogood
|
|
29
|
+
* into a shared append-only store node (the same race-free primitive `_markDivergent`
|
|
30
|
+
* uses) — `recordNogood(...)` builds that mutation fragment;
|
|
31
|
+
* - a cheap upstream `Nogood::guard` concept reads the store and, for each nogood matching
|
|
32
|
+
* this object's context, sets a DISCRETE typed `skip_<trial>:true` flag (barrier-clean —
|
|
33
|
+
* the hot gate keys on a typed boolean, never on the store);
|
|
34
|
+
* - the expensive trial gains `require:['NogoodGuard']` (DEFER the cast until the cheap
|
|
35
|
+
* guard has run) + `ensure:['!$skip_<trial>']` (sound-skip) — `guardTrial(...)` adds both.
|
|
36
|
+
*
|
|
37
|
+
* Two ordering disciplines are load-bearing (B, critique-driven) and baked in here:
|
|
38
|
+
* (1) the trial's `require` lists the guard, so an unresolved require DEFERS the expensive
|
|
39
|
+
* cast until the cheap guard's self-flag appears (the frontier order is not host-
|
|
40
|
+
* controllable per concept — the require-deferral is the existing-system route);
|
|
41
|
+
* (2) the guard writes the `skip_*` flags FIRST and its OWN self-flag LAST, so when the
|
|
42
|
+
* trial's require-watcher re-tests (the moment the self-flag lands) every skip flag is
|
|
43
|
+
* already set — otherwise an intra-mutation key-order race lets a trial fire first.
|
|
44
|
+
*
|
|
45
|
+
* A purely ADVISORY hint (a fact nothing gates on) saved ZERO work in B — soft preference
|
|
46
|
+
* needs a core `cfg.frontierComparator` (P1, not built); the sound-skip here is the safe,
|
|
47
|
+
* fixpoint-preserving existing-system answer. Per-episode reset by default (clear the store
|
|
48
|
+
* between episodes; GitOfThoughts-style) — keep it only as long as the context holds.
|
|
49
|
+
*
|
|
50
|
+
* const { createNogood, recordNogood, guardTrial, nogoodGuardConcept } = require('./nogood');
|
|
51
|
+
* register(Graph, [ createNogood() ]); // wires Nogood::guard
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the nogood-store push fragment a trial's provider emits on a dead end (score 0).
|
|
56
|
+
* Spread it into the provider's mutation array alongside the trial's own self-flag write.
|
|
57
|
+
* @param opts.memId the shared store node id (default 'mem')
|
|
58
|
+
* @param opts.storeKey the array fact on it (default 'nogoods')
|
|
59
|
+
* @param opts.ctxKey the canonical context key this dead end is keyed on (e.g. the segment kind)
|
|
60
|
+
* @param opts.trial the trial concept name that is dead for this context
|
|
61
|
+
* @returns a `{ $$_id, <storeKey>: { __push: {ctxKey,trial} } }` mutation template
|
|
62
|
+
*/
|
|
63
|
+
function recordNogood( opts ) {
|
|
64
|
+
opts = opts || {};
|
|
65
|
+
var tpl = { $$_id: opts.memId || 'mem' };
|
|
66
|
+
tpl[opts.storeKey || 'nogoods'] = { __push: { ctxKey: opts.ctxKey, trial: opts.trial } };
|
|
67
|
+
return tpl;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build the nogood guard provider fragment (host opt-in, like createVerifier).
|
|
72
|
+
* @returns { Nogood: { guard } }
|
|
73
|
+
*
|
|
74
|
+
* Concept wiring (the cheap upstream guard):
|
|
75
|
+
* { require:['Trial','kind'], provider:['Nogood::guard'],
|
|
76
|
+
* nogood:{ memId:'mem', storeKey:'nogoods', ctxKeyField:'kind' } }
|
|
77
|
+
* Reads the shared store, and for each nogood whose ctxKey matches this object's
|
|
78
|
+
* `<ctxKeyField>` writes `skip_<trial>:true`, then its OWN self-flag LAST.
|
|
79
|
+
*/
|
|
80
|
+
function createNogood( opts ) {
|
|
81
|
+
opts = opts || {};
|
|
82
|
+
return {
|
|
83
|
+
Nogood: {
|
|
84
|
+
guard: function ( graph, concept, scope, argz, cb ) {
|
|
85
|
+
var cfg = Object.assign({ memId: 'mem', storeKey: 'nogoods', ctxKeyField: 'kind' },
|
|
86
|
+
concept._schema && concept._schema.nogood, argz && argz[0]),
|
|
87
|
+
memEtty = graph.getEtty(cfg.memId),
|
|
88
|
+
store = (memEtty && memEtty._ && memEtty._[cfg.storeKey]) || [],
|
|
89
|
+
ctx = scope._[cfg.ctxKeyField],
|
|
90
|
+
out = { $_id: '_parent' };
|
|
91
|
+
// skip_* flags FIRST …
|
|
92
|
+
for ( var i = 0; i < store.length; i++ ) {
|
|
93
|
+
var ng = store[i];
|
|
94
|
+
if ( ng && ng.ctxKey === ctx ) out['skip_' + ng.trial] = true;
|
|
95
|
+
}
|
|
96
|
+
// … the guard's OWN self-flag LAST (so the trial's require-watcher re-tests with
|
|
97
|
+
// every skip flag already set — discipline (2) above).
|
|
98
|
+
out[concept._name] = true;
|
|
99
|
+
cb(null, out);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add the sound-skip discipline to a trial concept schema: defer on the guard's self-flag
|
|
107
|
+
* (require) and skip when the learned nogood says so (ensure). Returns a NEW schema.
|
|
108
|
+
* @param schema the trial concept schema
|
|
109
|
+
* @param opts.trial the skip-flag basis (default schema._name)
|
|
110
|
+
* @param opts.guard the guard concept self-flag name to require (default 'NogoodGuard')
|
|
111
|
+
*/
|
|
112
|
+
function guardTrial( schema, opts ) {
|
|
113
|
+
opts = opts || {};
|
|
114
|
+
var name = opts.trial || schema._name,
|
|
115
|
+
guard = opts.guard || 'NogoodGuard',
|
|
116
|
+
asArr = function ( v ) { return v == null ? [] : Array.isArray(v) ? v.slice() : [v]; },
|
|
117
|
+
out = Object.assign({}, schema);
|
|
118
|
+
out.require = asArr(schema.require).concat([guard]);
|
|
119
|
+
out.ensure = asArr(schema.ensure).concat(['!$skip_' + name]);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The cheap upstream guard concept (provider-less wrapper) for a host that wants it ready-made.
|
|
125
|
+
* @param opts.require what marks an object as guardable (default ['Trial','kind'])
|
|
126
|
+
* @param opts.memId/storeKey/ctxKeyField passed through to the provider config
|
|
127
|
+
* @param opts.name the guard self-flag name (default 'NogoodGuard')
|
|
128
|
+
*/
|
|
129
|
+
function nogoodGuardConcept( opts ) {
|
|
130
|
+
opts = opts || {};
|
|
131
|
+
var name = opts.name || 'NogoodGuard';
|
|
132
|
+
return {
|
|
133
|
+
_id: opts.id || name, _name: name,
|
|
134
|
+
require: opts.require || ['Trial', opts.ctxKeyField || 'kind'],
|
|
135
|
+
provider: ['Nogood::guard'],
|
|
136
|
+
nogood: {
|
|
137
|
+
memId: opts.memId || 'mem',
|
|
138
|
+
storeKey: opts.storeKey || 'nogoods',
|
|
139
|
+
ctxKeyField: opts.ctxKeyField || 'kind'
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
recordNogood: recordNogood,
|
|
146
|
+
createNogood: createNogood,
|
|
147
|
+
guardTrial: guardTrial,
|
|
148
|
+
nogoodGuardConcept: nogoodGuardConcept
|
|
149
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* Reason-loop — the packaged `AI::*` provider set that drives `concepts/_substrate` end-to-end.
|
|
21
|
+
* The LLM generalization of `examples/poc/trip-decompose.js#makeTripProviders` (which used canned
|
|
22
|
+
* content). Roadmap P1.a / GAP A of `doc/WIP/2026-07-05-combos-design.md §11`.
|
|
23
|
+
*
|
|
24
|
+
* WHY THIS EXISTS. `concepts/_substrate/Task/*` + `Claim/*` NAME `AI::evalComplexity/expand/answer/
|
|
25
|
+
* reportUp/rollup/confidence` but the library ships NO `AI::` provider — so the answer loop never
|
|
26
|
+
* runs: an unwired provider silently auto-flags its concept (Concept.js:239) without writing
|
|
27
|
+
* `complexityClass`, so `Atomic`/`Compound` never cast and no `Answer` is ever produced. This brick
|
|
28
|
+
* supplies the set, each fn emitting the EXACT marker its concept gates on (mirroring the proven
|
|
29
|
+
* trip markers — `tests/integration/poc-decompose.test.js`):
|
|
30
|
+
*
|
|
31
|
+
* Task/EvalComplexity -> { EvalComplexity, complexityClass:'atomic'|'compound' } (enum, canon-snapped)
|
|
32
|
+
* .../Compound/Expansion-> { Expansion, expandedInto } + child Node/Segment templates (the AND hyper-edge)
|
|
33
|
+
* Task/Answer -> { Answer, answer } (concept's own applyMutations sets `Answered`)
|
|
34
|
+
* Task/ReportUp -> push self-id into parent.answeredBy (deterministic, race-free {__push})
|
|
35
|
+
* Task/Rollup -> { Rollup, answer } (fires on the .length completion gate)
|
|
36
|
+
* Claim/Confidence -> { Confidence, confBand } (a SNAPPED band, never a raw float — K1/A2)
|
|
37
|
+
*
|
|
38
|
+
* Plus the INTAKE→TASK BRIDGE (GAP B): `AI::seedTask` turns a typed Intake node into a root Task
|
|
39
|
+
* Segment carrying the question, so the answer loop has something to run on (without it the intake
|
|
40
|
+
* and the loop are two disconnected sub-graphs). A host wires the matching concept
|
|
41
|
+
* `concepts/_substrate/Intake/ToTask.json` (require Intake, ensure IntakeStatus=='typed').
|
|
42
|
+
*
|
|
43
|
+
* TERMINATION + TYPED REFUSAL (answer-side). A depth FLOOR forces `atomic` at `maxDepth`. A model
|
|
44
|
+
* reply that can't be snapped to a class (eval) or yields no steps (expand) emits `strategiesExhausted`
|
|
45
|
+
* WITHOUT a class/children — so the task neither answers nor expands and lands in `Frontier/Stuck`
|
|
46
|
+
* (`ensure:["$strategiesExhausted"]`, previously never written — GAP C), a readable answer-side refusal,
|
|
47
|
+
* instead of re-firing to the apply-cap. `strategiesExhausted` is placed only on eval/expand (whose
|
|
48
|
+
* concepts do NOT force `Answered`); the answer concept's own `applyMutations` sets `Answered`, so an
|
|
49
|
+
* answer-call failure is surfaced as an errored answer (`llmError`), not a stuck state.
|
|
50
|
+
*
|
|
51
|
+
* FAITHFUL to the substrate authoring: each provider USES the concept's declared `prompt` (interpolated
|
|
52
|
+
* `${label}` etc.) and, where the concept declares `prompt.facts`, the canonicalization barrier
|
|
53
|
+
* (`canonicalize.canonFacts`) to snap the enum — the same K1 discipline as `LLM::complete`/`Intake`.
|
|
54
|
+
* Provider-cast-marker GOTCHA honored: every returned template self-flags its concept `_name`.
|
|
55
|
+
*
|
|
56
|
+
* @param opts.ask async ({system,user,maxTokens}) -> string. REQUIRED (throws otherwise).
|
|
57
|
+
* @param opts.parseJSON JSON-salvage fn. Default `llm.parseJSON`.
|
|
58
|
+
* @param opts.maxDepth decomposition depth floor (forces atomic at/after it). Default 2.
|
|
59
|
+
* @param opts.maxBranch max sub-steps per expand. Default 5.
|
|
60
|
+
* @param opts.namespace provider namespace. Default 'AI'.
|
|
61
|
+
* @returns { AI: { seedTask, evalComplexity, expand, answer, reportUp, rollup, confidence } }
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
var llm = require('./llm');
|
|
65
|
+
var canonicalize = require('./canonicalize');
|
|
66
|
+
|
|
67
|
+
function createReasonLoop( opts ) {
|
|
68
|
+
opts = opts || {};
|
|
69
|
+
var ask = opts.ask;
|
|
70
|
+
if ( typeof ask !== 'function' )
|
|
71
|
+
throw new Error('createReasonLoop needs opts.ask (an async ({system,user,maxTokens}) -> string)');
|
|
72
|
+
var parseJSON = opts.parseJSON || llm.parseJSON,
|
|
73
|
+
maxDepth = opts.maxDepth == null ? 2 : opts.maxDepth,
|
|
74
|
+
maxBranch = opts.maxBranch || 5,
|
|
75
|
+
namespace = opts.namespace || 'AI';
|
|
76
|
+
|
|
77
|
+
// the task's text: the segment label (the sub-problem) or the raw question on a seed node.
|
|
78
|
+
function taskText( scope ) { return scope._.rawText || scope._.label || ''; }
|
|
79
|
+
|
|
80
|
+
// the concept's authored prompt (interpolated), with a default when the concept ships none.
|
|
81
|
+
function promptOf( concept, scope, graph, dflt ) {
|
|
82
|
+
var p = (concept._schema && concept._schema.prompt) || {};
|
|
83
|
+
return {
|
|
84
|
+
system : llm.interpolate(p.system, graph, scope) || dflt.system,
|
|
85
|
+
user : llm.interpolate(p.user, graph, scope) || dflt.user,
|
|
86
|
+
facts : p.facts,
|
|
87
|
+
prose : p.prose
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function trace( graph, concept, scope, io ) { if ( graph.traceProvider ) graph.traceProvider(concept, scope, io); }
|
|
91
|
+
|
|
92
|
+
var ns = {};
|
|
93
|
+
ns[namespace] = {
|
|
94
|
+
// ── BRIDGE (GAP B): a typed intake seeds a root Task Segment carrying the question ──────────
|
|
95
|
+
seedTask: function ( graph, concept, scope, argz, cb ) {
|
|
96
|
+
var id = scope._._id,
|
|
97
|
+
q = scope._.rawText || scope._.intakeNarrative || scope._.label || '';
|
|
98
|
+
cb(null, [
|
|
99
|
+
{ $_id: '_parent', ToTask: true }, // self-flag (GOTCHA)
|
|
100
|
+
{ _id: id + '_start', Node: true, label: '(question)' }, // fresh endpoints (NOT the intake node,
|
|
101
|
+
{ _id: id + '_goal', Node: true, label: '(answer)' }, // which is not a segment endpoint)
|
|
102
|
+
{ _id: id + '_task', Segment: true, originNode: id + '_start', targetNode: id + '_goal',
|
|
103
|
+
depth: 0, label: q } // the root Task (label, NOT rawText:
|
|
104
|
+
// rawText re-triggers Intake → runaway)
|
|
105
|
+
]);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// ── EvalComplexity: atomic vs compound, canon-snapped to the concept's enum ────────────────
|
|
109
|
+
evalComplexity: function ( graph, concept, scope, argz, cb ) {
|
|
110
|
+
var depth = scope._.depth || 0;
|
|
111
|
+
if ( depth >= maxDepth ) // depth floor → terminate
|
|
112
|
+
return cb(null, { $_id: '_parent', EvalComplexity: true, complexityClass: 'atomic' });
|
|
113
|
+
var pr = promptOf(concept, scope, graph, {
|
|
114
|
+
system: 'Classify whether the task is "atomic" (answerable directly in one step) or "compound" (needs breaking into sub-steps).',
|
|
115
|
+
user : 'Task: ' + taskText(scope) }),
|
|
116
|
+
schema = pr.facts || { complexityClass: { enum: ['atomic', 'compound'] } };
|
|
117
|
+
Promise.resolve()
|
|
118
|
+
.then(function () { return ask({ system: pr.system + ' Reply ONLY JSON: {"complexityClass":"atomic"|"compound"}.', user: pr.user, maxTokens: 80 }); })
|
|
119
|
+
.then(function ( txt ) {
|
|
120
|
+
trace(graph, concept, scope, { reply: txt });
|
|
121
|
+
var cf = canonicalize.canonFacts(parseJSON(txt), schema);
|
|
122
|
+
if ( cf.misses.length || !cf.facts.complexityClass ) // no snappable class → no strategy
|
|
123
|
+
return cb(null, { $_id: '_parent', EvalComplexity: true, strategiesExhausted: true });
|
|
124
|
+
cb(null, { $_id: '_parent', EvalComplexity: true, complexityClass: cf.facts.complexityClass });
|
|
125
|
+
})
|
|
126
|
+
.catch(function ( e ) { cb(null, { $_id: '_parent', EvalComplexity: true, strategiesExhausted: true, llmError: e.message }); });
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ── Expansion: the AND hyper-edge of child sub-segments ────────────────────────────────────
|
|
130
|
+
expand: function ( graph, concept, scope, argz, cb ) {
|
|
131
|
+
var pr = promptOf(concept, scope, graph, {
|
|
132
|
+
system: 'Break the task into 2-' + maxBranch + ' ordered sub-steps.',
|
|
133
|
+
user : 'Task: ' + taskText(scope) });
|
|
134
|
+
Promise.resolve()
|
|
135
|
+
.then(function () { return ask({ system: pr.system + ' Reply ONLY JSON: {"steps":["...","..."]}.', user: pr.user, maxTokens: 400 }); })
|
|
136
|
+
.then(function ( txt ) {
|
|
137
|
+
trace(graph, concept, scope, { reply: txt });
|
|
138
|
+
var r = parseJSON(txt),
|
|
139
|
+
steps = (r && Array.isArray(r.steps) ? r.steps : []).slice(0, maxBranch)
|
|
140
|
+
.map(function ( s ) { return typeof s === 'string' ? s : (s && (s.name || s.step || s.label)) || ''; })
|
|
141
|
+
.filter(Boolean);
|
|
142
|
+
if ( !steps.length ) // couldn't decompose → stuck
|
|
143
|
+
return cb(null, { $_id: '_parent', Expansion: true, strategiesExhausted: true });
|
|
144
|
+
var base = scope._._id, origin = scope._.originNode, target = scope._.targetNode,
|
|
145
|
+
depth = (scope._.depth || 0) + 1,
|
|
146
|
+
childIds = steps.map(function ( _, i ) { return base + '_s' + i; }),
|
|
147
|
+
tpl = [{ $_id: '_parent', Expansion: true, expandedInto: childIds }],
|
|
148
|
+
prev = origin;
|
|
149
|
+
steps.forEach(function ( name, i ) {
|
|
150
|
+
var last = i === steps.length - 1, tnode = last ? target : base + '_m' + i;
|
|
151
|
+
if ( !last ) tpl.push({ _id: tnode, Node: true, label: name });
|
|
152
|
+
tpl.push({ _id: childIds[i], Segment: true, originNode: prev, targetNode: tnode,
|
|
153
|
+
depth: depth, parentSeg: base, label: name }); // label only (no rawText → no re-intake)
|
|
154
|
+
prev = tnode;
|
|
155
|
+
});
|
|
156
|
+
cb(null, tpl);
|
|
157
|
+
})
|
|
158
|
+
.catch(function ( e ) { cb(null, { $_id: '_parent', Expansion: true, strategiesExhausted: true, llmError: e.message }); });
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ── Answer: terminal prose for an atomic leaf (concept applyMutations sets `Answered`) ──────
|
|
162
|
+
answer: function ( graph, concept, scope, argz, cb ) {
|
|
163
|
+
var pr = promptOf(concept, scope, graph, {
|
|
164
|
+
system: 'Answer the task directly and concisely (max ~4 sentences).',
|
|
165
|
+
user : 'Task: ' + taskText(scope) });
|
|
166
|
+
Promise.resolve()
|
|
167
|
+
.then(function () { return ask({ system: pr.system, user: pr.user, maxTokens: 500 }); })
|
|
168
|
+
.then(function ( txt ) {
|
|
169
|
+
trace(graph, concept, scope, { reply: txt });
|
|
170
|
+
cb(null, { $_id: '_parent', Answer: true, answer: String(txt).trim() });
|
|
171
|
+
})
|
|
172
|
+
.catch(function ( e ) { cb(null, { $_id: '_parent', Answer: true, answer: '(answer unavailable)', llmError: e.message }); });
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// ── ReportUp: race-free stigmergic fan-in (no LLM) ─────────────────────────────────────────
|
|
176
|
+
reportUp: function ( graph, concept, scope, argz, cb ) {
|
|
177
|
+
cb(null, [
|
|
178
|
+
{ $$_id: scope._.parentSeg, answeredBy: { __push: scope._._id } },
|
|
179
|
+
{ $_id: '_parent', ReportUp: true }
|
|
180
|
+
]);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// ── Rollup: bounded bottom-up synthesis, fires once on the .length gate ─────────────────────
|
|
184
|
+
rollup: function ( graph, concept, scope, argz, cb ) {
|
|
185
|
+
var kids = (scope._.expandedInto || [])
|
|
186
|
+
.map(function ( id ) { var e = graph.getEtty(id); return e && e._.answer; })
|
|
187
|
+
.filter(function ( a ) { return a != null; });
|
|
188
|
+
var pr = promptOf(concept, scope, graph, {
|
|
189
|
+
system: 'Synthesize the sub-answers into ONE coherent, BOUNDED answer (max ~6 sentences). Do not copy; summarize.',
|
|
190
|
+
user : null });
|
|
191
|
+
Promise.resolve()
|
|
192
|
+
.then(function () {
|
|
193
|
+
return ask({ system: pr.system,
|
|
194
|
+
user: (pr.user ? pr.user + '\n\n' : 'Task: ' + taskText(scope) + '\n\n') +
|
|
195
|
+
'Sub-answers:\n' + kids.map(function ( a, i ) { return (i + 1) + '. ' + a; }).join('\n'),
|
|
196
|
+
maxTokens: 700 });
|
|
197
|
+
})
|
|
198
|
+
.then(function ( txt ) {
|
|
199
|
+
trace(graph, concept, scope, { reply: txt });
|
|
200
|
+
cb(null, { $_id: '_parent', Rollup: true, answer: String(txt).trim() });
|
|
201
|
+
})
|
|
202
|
+
.catch(function () { cb(null, { $_id: '_parent', Rollup: true, answer: kids.join(' ') }); }); // fail-soft: concat
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// ── Confidence: a SNAPPED band, canon-checked against the concept's enum ─────────────────────
|
|
206
|
+
confidence: function ( graph, concept, scope, argz, cb ) {
|
|
207
|
+
var pr = promptOf(concept, scope, graph, {
|
|
208
|
+
system: 'Rate confidence in the answer as one of: low, medium, high.',
|
|
209
|
+
user : 'Task: ' + taskText(scope) + '\nAnswer: ' + (scope._.answer || '') }),
|
|
210
|
+
schema = pr.facts || { confBand: { enum: ['low', 'medium', 'high'] } };
|
|
211
|
+
Promise.resolve()
|
|
212
|
+
.then(function () { return ask({ system: pr.system + ' Reply ONLY JSON: {"confBand":"low"|"medium"|"high"}.', user: pr.user, maxTokens: 40 }); })
|
|
213
|
+
.then(function ( txt ) {
|
|
214
|
+
var cf = canonicalize.canonFacts(parseJSON(txt), schema);
|
|
215
|
+
cb(null, { $_id: '_parent', Confidence: true, confBand: cf.facts.confBand || 'low' }); // OOB → fail-closed low
|
|
216
|
+
})
|
|
217
|
+
.catch(function () { cb(null, { $_id: '_parent', Confidence: true, confBand: 'low' }); });
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
return ns;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { createReasonLoop: createReasonLoop };
|
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* Semiring-parameterized accumulation — D and P are ONE forward-chaining machine
|
|
21
|
+
* parameterized by the certainty semiring (doc/WIP/HANDOFF.md §7 Tier 2; SOTA
|
|
22
|
+
* subgraph-grammars: provenance semirings, Green-Tannen 2007; experiments E1/E4). Zero
|
|
23
|
+
* core change: it packages the coherence-theorem-validated commutative-monoid REDUCE
|
|
24
|
+
* (E1: fold `{__push}`ed contributions after quiescence; order-independence ⟺ the combine
|
|
25
|
+
* is a commutative monoid) as a reusable provider whose `⊕`/identity/readout you choose:
|
|
26
|
+
*
|
|
27
|
+
* - `boolean` (the D socle): ⊕ = OR, readout = identity -> "does ANY contribution hold"
|
|
28
|
+
* - `logodds` (the P layer): ⊕ = +, readout = σ -> certainty accumulation (E1/E4)
|
|
29
|
+
* - `maxplus` (best-path): ⊕ = max, readout = identity -> Viterbi / longest-evidence
|
|
30
|
+
* - `probor` (noisy-OR): ⊕ = a+b−ab on [0,1] -> independent-evidence OR
|
|
31
|
+
*
|
|
32
|
+
* The same engine, the same fold-after-quiescence, a different algebra — adding a same-family
|
|
33
|
+
* regime is "declare a semiring", not a core change. Keep `⊕` a COMMUTATIVE monoid or the
|
|
34
|
+
* result depends on the (non-semantic) scheduler order (E1: monoid variance ≈0 vs a
|
|
35
|
+
* non-commutative combine ranging [−0.41, 1.06] on the SAME contributions).
|
|
36
|
+
*
|
|
37
|
+
* const { createSemiring, semiringConceptTree } = require('./semiring');
|
|
38
|
+
* register(Graph, [ createSemiring() ]); // wires Semiring::reduce
|
|
39
|
+
*
|
|
40
|
+
* Contributions arrive via the race-free `{__push}` primitive into an array fact; a `Reduce`
|
|
41
|
+
* concept gated `ensure:["$contribs.length==$expected"]` (the proven completion gate) folds
|
|
42
|
+
* them after the stratum is quiescent — same shape as verify's Vote::tally and stats' grandMean.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
var mc = require('./merge-consistency');
|
|
46
|
+
var sigmoid = mc.sigmoid, bandOf = mc.bandOf;
|
|
47
|
+
|
|
48
|
+
// Built-in commutative semirings: { plus, zero, times, one, readout }. `plus`/`zero` drive the
|
|
49
|
+
// REDUCE; `times`/`one` are the ⊗ for combining along a path (exposed for completeness).
|
|
50
|
+
var SEMIRINGS = {
|
|
51
|
+
boolean: { plus: function ( a, b ) { return !!(a || b); }, zero: false, times: function ( a, b ) { return !!(a && b); }, one: true, readout: function ( x ) { return x; } },
|
|
52
|
+
logodds: { plus: function ( a, b ) { return a + b; }, zero: 0, times: function ( a, b ) { return a + b; }, one: 0, readout: sigmoid },
|
|
53
|
+
maxplus: { plus: function ( a, b ) { return Math.max(a, b); }, zero: -Infinity, times: function ( a, b ) { return a + b; }, one: 0, readout: function ( x ) { return x; } },
|
|
54
|
+
probor: { plus: function ( a, b ) { return a + b - a * b; }, zero: 0, times: function ( a, b ) { return a * b; }, one: 1, readout: function ( x ) { return x; } }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function resolveSemiring( sr ) {
|
|
58
|
+
if ( !sr ) return SEMIRINGS.logodds;
|
|
59
|
+
return typeof sr === 'string' ? (SEMIRINGS[sr] || SEMIRINGS.logodds) : sr;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fold a list of contributions under a semiring's ⊕ (commutative -> order-independent), then
|
|
64
|
+
* apply the readout. The accumulation is the abstract value; the readout is the observable.
|
|
65
|
+
* @param values the contributions (`{__push}`ed)
|
|
66
|
+
* @param sr a built-in name or a `{ plus, zero, readout }` object (default 'logodds')
|
|
67
|
+
* @returns { acc, value, n } acc = Σ⊕ contributions, value = readout(acc)
|
|
68
|
+
*/
|
|
69
|
+
function reduceSemiring( values, sr ) {
|
|
70
|
+
sr = resolveSemiring(sr);
|
|
71
|
+
var xs = values || [], acc = sr.zero;
|
|
72
|
+
for ( var i = 0; i < xs.length; i++ ) acc = sr.plus(acc, xs[i]);
|
|
73
|
+
return { acc: acc, value: sr.readout ? sr.readout(acc) : acc, n: xs.length };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- Pareto / skyline selection (the multi-criteria SELECT operator for the support grammar's
|
|
77
|
+
// Candidate/Selected cluster). Laurie's framing: skyline-of-union is an IDEMPOTENT COMMUTATIVE
|
|
78
|
+
// MONOID, so it folds in reduceSemiring exactly like any semiring and inherits E1 order-
|
|
79
|
+
// independence; criteria scored on DISCRETE BAND RANKS keep dominance deterministic & memoizable
|
|
80
|
+
// (the float skyline is fragile — re-keys the K1 memo) and bound the front size. ----
|
|
81
|
+
|
|
82
|
+
// Rank of a value under a criterion spec — HIGHER RANK = BETTER, always (polarity folded in here,
|
|
83
|
+
// so dominance never silently selects the worst). A spec is either an ORDERED band list worst→best
|
|
84
|
+
// (the order IS the preference direction) or { dir:'max'|'min' } for a numeric criterion. A
|
|
85
|
+
// missing/unknown band ranks below all, so it never spuriously dominates.
|
|
86
|
+
function rankOf( value, spec ) {
|
|
87
|
+
if ( Array.isArray(spec) ) { var i = spec.indexOf(value); return i < 0 ? -1 : i; }
|
|
88
|
+
if ( spec && spec.order ) { var j = spec.order.indexOf(value); return j < 0 ? -1 : j; }
|
|
89
|
+
var n = Number(value);
|
|
90
|
+
if ( !isFinite(n) ) return -Infinity;
|
|
91
|
+
return (spec && spec.dir === 'min') ? -n : n; // min: lower is better -> negate
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strict Pareto dominance: a ≥ b on EVERY criterion AND > on at least one. Strict (not weak), so
|
|
95
|
+
// mutually-equal points do NOT dominate each other — ties are kept.
|
|
96
|
+
function dominates( a, b, criteria ) {
|
|
97
|
+
var keys = Object.keys(criteria), strictly = false;
|
|
98
|
+
for ( var i = 0; i < keys.length; i++ ) {
|
|
99
|
+
var ra = rankOf(a[keys[i]], criteria[keys[i]]), rb = rankOf(b[keys[i]], criteria[keys[i]]);
|
|
100
|
+
if ( ra < rb ) return false;
|
|
101
|
+
if ( ra > rb ) strictly = true;
|
|
102
|
+
}
|
|
103
|
+
return strictly;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The skyline: points not strictly dominated by any other. Stable (input order preserved); ties
|
|
107
|
+
// all kept. O(n²·d) block-nested-loop — trivial at this scale (a handful of candidates); no index
|
|
108
|
+
// or pre-sort is justified.
|
|
109
|
+
function paretoFront( points, criteria ) {
|
|
110
|
+
var pts = points || [], out = [];
|
|
111
|
+
for ( var i = 0; i < pts.length; i++ ) {
|
|
112
|
+
var dominated = false;
|
|
113
|
+
for ( var j = 0; j < pts.length; j++ )
|
|
114
|
+
if ( i !== j && dominates(pts[j], pts[i], criteria) ) { dominated = true; break; }
|
|
115
|
+
if ( !dominated ) out.push(pts[i]);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Étage-2 tie-break: lexicographic by DESCENDING rank along a criterion-priority list.
|
|
121
|
+
function lexCompare( a, b, criteria, lex ) {
|
|
122
|
+
for ( var i = 0; i < lex.length; i++ ) {
|
|
123
|
+
var d = rankOf(b[lex[i]], criteria[lex[i]]) - rankOf(a[lex[i]], criteria[lex[i]]); // desc = better first
|
|
124
|
+
if ( d ) return d;
|
|
125
|
+
}
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Pareto SELECT = étage 1 (front, no weighting) + étage 2 (tie-break, default lexicographic on the
|
|
130
|
+
// criteria order; a custom `opts.tieBreak(a,b)` overrides), with a FINAL deterministic id tie-break
|
|
131
|
+
// so the pick is reproducible run-to-run (a hard tie must not depend on iteration order). idKey='id'.
|
|
132
|
+
function paretoSelect( points, criteria, opts ) {
|
|
133
|
+
opts = opts || {};
|
|
134
|
+
var idKey = opts.idKey || 'id',
|
|
135
|
+
lex = opts.lex || Object.keys(criteria),
|
|
136
|
+
cmp = opts.tieBreak || function ( a, b ) { return lexCompare(a, b, criteria, lex); },
|
|
137
|
+
front = paretoFront(points, criteria),
|
|
138
|
+
ranked = front.slice().sort(function ( a, b ) {
|
|
139
|
+
return cmp(a, b) || String(a[idKey]).localeCompare(String(b[idKey]));
|
|
140
|
+
}),
|
|
141
|
+
selected = ranked[0] || null;
|
|
142
|
+
return {
|
|
143
|
+
front : front,
|
|
144
|
+
frontIds : front.map(function ( p ) { return p[idKey]; }),
|
|
145
|
+
ranked : ranked,
|
|
146
|
+
selected : selected,
|
|
147
|
+
selectedId: selected ? selected[idKey] : null,
|
|
148
|
+
n : (points || []).length
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Pareto AS A SEMIRING for reduceSemiring: plus = skyline(front ∪ {point}) RECOMPUTED (never an
|
|
153
|
+
// incremental shortcut — that would break associativity → E1 order-independence), zero = ∅,
|
|
154
|
+
// readout = the front. So `reduceSemiring(points, makePareto(crit)).value` is the order-invariant skyline.
|
|
155
|
+
function makePareto( criteria ) {
|
|
156
|
+
return {
|
|
157
|
+
plus : function ( front, point ) { return paretoFront(front.concat([point]), criteria); },
|
|
158
|
+
zero : [],
|
|
159
|
+
readout: function ( front ) { return front; }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build the semiring reducer provider fragment (host opt-in, like createVerifier).
|
|
165
|
+
* @param opts.semirings extra named semirings merged over the built-ins.
|
|
166
|
+
* @returns { Semiring: { reduce } }
|
|
167
|
+
*
|
|
168
|
+
* Concept wiring:
|
|
169
|
+
* { require:['Pool'], ensure:['$contribs.length==$expected'], provider:['Semiring::reduce'],
|
|
170
|
+
* semiring:{ contribKey:'contribs', semiring:'logodds', as:'', bands:<optional> } }
|
|
171
|
+
* Emits the self-flag + <as>acc (the ⊕-fold) + <as>value (readout) + <as>n, and — when a
|
|
172
|
+
* `bands` table is given — a SNAPPED <as>band / <as>bandRank (barrier-clean enum to gate on).
|
|
173
|
+
*/
|
|
174
|
+
function createSemiring( opts ) {
|
|
175
|
+
opts = opts || {};
|
|
176
|
+
var registry = Object.assign({}, SEMIRINGS, opts.semirings);
|
|
177
|
+
return {
|
|
178
|
+
Semiring: {
|
|
179
|
+
reduce: function ( graph, concept, scope, argz, cb ) {
|
|
180
|
+
var cfg = Object.assign({ contribKey: 'contribs', semiring: 'logodds', as: '' },
|
|
181
|
+
concept._schema && concept._schema.semiring, argz && argz[0]),
|
|
182
|
+
vals = graph.getRef(cfg.contribKey, scope) || [],
|
|
183
|
+
as = cfg.as || '',
|
|
184
|
+
facts = { $_id: '_parent' };
|
|
185
|
+
facts[concept._name] = true;
|
|
186
|
+
|
|
187
|
+
// pareto family: multi-criteria SELECT (front + tie-break). Emits a barrier-clean
|
|
188
|
+
// discrete selectedId + the front ids (NOT the candidate objects) for the cluster to gate on.
|
|
189
|
+
if ( cfg.semiring === 'pareto' ) {
|
|
190
|
+
var sel = paretoSelect(vals, cfg.criteria || {}, { idKey: cfg.idKey, lex: cfg.lex, tieBreak: cfg.tieBreak });
|
|
191
|
+
facts[as + 'selectedId'] = sel.selectedId;
|
|
192
|
+
facts[as + 'frontSize'] = sel.front.length;
|
|
193
|
+
facts[as + 'frontIds'] = sel.frontIds;
|
|
194
|
+
facts[as + 'n'] = sel.n;
|
|
195
|
+
return cb(null, facts);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var sr = typeof cfg.semiring === 'string' ? (registry[cfg.semiring] || SEMIRINGS.logodds) : cfg.semiring,
|
|
199
|
+
r = reduceSemiring(vals, sr);
|
|
200
|
+
facts[as + 'acc'] = r.acc;
|
|
201
|
+
facts[as + 'value'] = r.value;
|
|
202
|
+
facts[as + 'n'] = r.n;
|
|
203
|
+
if ( cfg.bands ) { // optional snapped readout (barrier-clean enum)
|
|
204
|
+
var b = bandOf(r.value, cfg.bands);
|
|
205
|
+
facts[as + 'band'] = b.label;
|
|
206
|
+
facts[as + 'bandRank'] = b.rank;
|
|
207
|
+
}
|
|
208
|
+
cb(null, facts);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A ready-made reducer concept fragment: contributions `{__push}` into `contribKey`, and a
|
|
216
|
+
* `Reduce` concept folds them under the semiring once the cardinality gate is satisfied.
|
|
217
|
+
* @param opts.semiring built-in name or a semiring object (default 'logodds')
|
|
218
|
+
* @param opts.contribKey the pushed-contributions array fact (default 'contribs')
|
|
219
|
+
* @param opts.require Reduce's require (default ['Pool'])
|
|
220
|
+
* @param opts.as output prefix
|
|
221
|
+
* @param opts.bands optional snapped-band table
|
|
222
|
+
*/
|
|
223
|
+
function semiringConceptTree( opts ) {
|
|
224
|
+
opts = opts || {};
|
|
225
|
+
var contribKey = opts.contribKey || 'contribs',
|
|
226
|
+
semiring = { contribKey: contribKey, semiring: opts.semiring || 'logodds', as: opts.as || '' };
|
|
227
|
+
if ( opts.bands ) semiring.bands = opts.bands;
|
|
228
|
+
return {
|
|
229
|
+
childConcepts: {
|
|
230
|
+
Reduce: {
|
|
231
|
+
_id: 'Reduce', _name: 'Reduce',
|
|
232
|
+
require: opts.require || ['Pool'],
|
|
233
|
+
ensure: ['$' + contribKey + '.length==$expected'],
|
|
234
|
+
provider: ['Semiring::reduce'],
|
|
235
|
+
semiring: semiring
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A ready-made multi-criteria SELECT concept (the support grammar's Candidate→Selected step, J2):
|
|
243
|
+
* candidate vectors `{__push}` into `contribKey`, and a `Select` concept folds them with the `pareto`
|
|
244
|
+
* semiring once the cardinality gate holds — emitting the Pareto front + a deterministic lexicographic
|
|
245
|
+
* pick (`selectedId`). Same shape as semiringConceptTree, specialized to the pareto family.
|
|
246
|
+
* @param opts.criteria {name: bandList|{dir}} the comparison criteria (REQUIRED to be useful)
|
|
247
|
+
* @param opts.lex criterion-priority order for the tie-break (default: criteria key order)
|
|
248
|
+
* @param opts.idKey candidate id key (default 'id')
|
|
249
|
+
* @param opts.contribKey pooled-candidates array fact (default 'candidates')
|
|
250
|
+
* @param opts.require Select's require (default ['Pool'])
|
|
251
|
+
* @param opts.as output prefix
|
|
252
|
+
*/
|
|
253
|
+
function selectConceptTree( opts ) {
|
|
254
|
+
opts = opts || {};
|
|
255
|
+
var contribKey = opts.contribKey || 'candidates',
|
|
256
|
+
semiring = { contribKey: contribKey, semiring: 'pareto', criteria: opts.criteria || {}, as: opts.as || '' };
|
|
257
|
+
if ( opts.lex ) semiring.lex = opts.lex;
|
|
258
|
+
if ( opts.idKey ) semiring.idKey = opts.idKey;
|
|
259
|
+
return {
|
|
260
|
+
childConcepts: {
|
|
261
|
+
Select: {
|
|
262
|
+
_id: 'Select', _name: 'Select',
|
|
263
|
+
require: opts.require || ['Pool'],
|
|
264
|
+
ensure: ['$' + contribKey + '.length==$expected'],
|
|
265
|
+
provider: ['Semiring::reduce'],
|
|
266
|
+
semiring: semiring
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
SEMIRINGS: SEMIRINGS,
|
|
274
|
+
reduceSemiring: reduceSemiring,
|
|
275
|
+
createSemiring: createSemiring,
|
|
276
|
+
semiringConceptTree: semiringConceptTree,
|
|
277
|
+
selectConceptTree: selectConceptTree,
|
|
278
|
+
// Pareto / skyline selection (the multi-criteria SELECT operator; a semiring that folds in reduceSemiring)
|
|
279
|
+
rankOf: rankOf,
|
|
280
|
+
dominates: dominates,
|
|
281
|
+
paretoFront: paretoFront,
|
|
282
|
+
lexCompare: lexCompare,
|
|
283
|
+
paretoSelect: paretoSelect,
|
|
284
|
+
makePareto: makePareto
|
|
285
|
+
};
|