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,189 @@
|
|
|
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
|
+
* C1 — the typed-QA APPLIANCE (roadmap P1 / design doc §3, §11). A THIN assembly over shipped
|
|
21
|
+
* bricks: the prose→typed front door (Intake), the packaged reason loop (createReasonLoop) over the
|
|
22
|
+
* `concepts/_substrate` grammar, the durable content-addressed memo (cache), and the answer/refusal
|
|
23
|
+
* projection — with the §4 product posture wired ON by default (defaults.js).
|
|
24
|
+
*
|
|
25
|
+
* `answer(q)` seeds the question as prose, settles, and projects a DISCRIMINATED result: either an
|
|
26
|
+
* `answered` result (the synthesized answer + its confidence band) or a TYPED `refused` result that
|
|
27
|
+
* NAMES the missing requirement (an intake barrier miss) or the exhausted strategy (an answer-side
|
|
28
|
+
* stuck) — never a wrong answer, never a bare failure. This is the differentiator: the system follows
|
|
29
|
+
* the typed SPEC (refuse when the input isn't faithfully typed) rather than world-plausibility.
|
|
30
|
+
*
|
|
31
|
+
* NO new logic lives here (anti-sclerosis): the appliance reads DISCRETE facts the bricks already
|
|
32
|
+
* emit (`IntakeStatus`, `IntakeMissing`, `answer`, `strategiesExhausted`, `confBand`) and chains
|
|
33
|
+
* brick calls. It never re-derives a verdict, and it never keys on a raw concept-name fact of a
|
|
34
|
+
* pure-assert concept (e.g. `Frontier` lingers after uncast — CLAUDE.md footgun); cast checks go
|
|
35
|
+
* through `_mappedConcepts`.
|
|
36
|
+
*
|
|
37
|
+
* @param opts.concepts domain concept dir(s) or a pre-built conceptMap (the `_substrate` reasoning
|
|
38
|
+
* grammar is always merged in). Omit for a neutral typed-QA appliance.
|
|
39
|
+
* @param opts.ask REQUIRED (opt-in): a function, or `{ localModel:'<gguf>' }` (embedded).
|
|
40
|
+
* @param opts.verify optional: `checks` for createVerifier (a verdict gates the Claim chain).
|
|
41
|
+
* @param opts.store optional: a memo store (default in-memory content-addressed cache).
|
|
42
|
+
* @param opts.maxDepth / opts.maxBranch reason-loop bounds (see createReasonLoop).
|
|
43
|
+
* @param opts.* the §4 knobs via resolveComboDefaults (failClosed/gate/memo/validate/…).
|
|
44
|
+
* @returns {{ answer, settle, graph, memo, close }}
|
|
45
|
+
* answer(question, opts?) => Promise<Result>
|
|
46
|
+
* Result = { status:'answered', answer, confBand, memoHit }
|
|
47
|
+
* | { status:'refused', reason:'untyped'|'partial'|'no-strategy', missing:[key…], prose }
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var path = require('path');
|
|
51
|
+
var Graph = require('../graph');
|
|
52
|
+
var defaults = require('./defaults.js');
|
|
53
|
+
var loadConceptMap = require('../load.js').loadConceptMap;
|
|
54
|
+
var providers = require('../providers');
|
|
55
|
+
var validateOrThrow = require('../authoring/validate.js').validateOrThrow;
|
|
56
|
+
var nextStable = require('../authoring/supervise.js').nextStable;
|
|
57
|
+
|
|
58
|
+
var SUBSTRATE_DIR = path.join(__dirname, '..', '..', 'concepts', '_substrate');
|
|
59
|
+
|
|
60
|
+
// a stable, content-derived id for a question (djb2). Same question → same intake node → same
|
|
61
|
+
// deterministic sub-tree ids → a repeat is served from the persisted sub-graph (0 model calls),
|
|
62
|
+
// and the leaf `answer` memo amortizes shared sub-steps across DIFFERENT questions.
|
|
63
|
+
function qidFor( question ) {
|
|
64
|
+
var h = 5381, s = String(question);
|
|
65
|
+
for ( var i = 0; i < s.length; i++ ) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
|
|
66
|
+
return '__ask_' + (h >>> 0).toString(36);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createAppliance( opts ) {
|
|
70
|
+
opts = opts || {};
|
|
71
|
+
var d = defaults.resolveComboDefaults(opts);
|
|
72
|
+
var ask = defaults.buildAsk(d); // throws if no backend (opt-in)
|
|
73
|
+
|
|
74
|
+
// ── 1. concept map: the domain set(s) + the reasoning substrate (always merged) ──────────────
|
|
75
|
+
var conceptMap;
|
|
76
|
+
if ( opts.conceptMap ) conceptMap = Object.assign({}, opts.conceptMap);
|
|
77
|
+
else {
|
|
78
|
+
var dirs = opts.concepts ? (Array.isArray(opts.concepts) ? opts.concepts.slice() : [opts.concepts]) : [];
|
|
79
|
+
dirs.push(SUBSTRATE_DIR);
|
|
80
|
+
conceptMap = loadConceptMap(dirs);
|
|
81
|
+
}
|
|
82
|
+
if ( !conceptMap._substrate ) Object.assign(conceptMap, loadConceptMap(SUBSTRATE_DIR));
|
|
83
|
+
var conceptSets = Object.keys(conceptMap);
|
|
84
|
+
|
|
85
|
+
// ── 2. author-time validation (default ON): reject prose-on-dependency-edges, missing _name … ──
|
|
86
|
+
if ( d.validate ) for ( var set in conceptMap ) validateOrThrow(conceptMap[set]);
|
|
87
|
+
|
|
88
|
+
// ── 3. providers: intake (front door) + the packaged reason loop + optional verifier ─────────
|
|
89
|
+
var intakeFrag = providers.createIntake({ ask: ask });
|
|
90
|
+
var reasonFrag = providers.createReasonLoop({ ask: ask, maxDepth: opts.maxDepth, maxBranch: opts.maxBranch });
|
|
91
|
+
var verifyFrag = opts.verify ? providers.createVerifier({ checks: opts.verify }) : null;
|
|
92
|
+
|
|
93
|
+
// ── 4. durable memo (default ON): wrap only the CONTENT-producing providers; the CanonMiss
|
|
94
|
+
// fail-closed in the cache keeps a non-typed intake un-cacheable (a refusal always
|
|
95
|
+
// re-calls, by design). STRUCTURAL providers are NOT memoized — expand/seedTask return
|
|
96
|
+
// templates with ids derived from the CURRENT segment, so a cached template carries stale
|
|
97
|
+
// ids and mis-wires a different question's sub-tree; and reportUp/rollup are deterministic
|
|
98
|
+
// / fire-once. Cross-question amortization comes from the leaf `answer` cache (a shared
|
|
99
|
+
// sub-step label hits); same-question 0-call replay comes from the content-stable id +
|
|
100
|
+
// the fast path in answer() (the sub-graph persists and is reused). ────────────────────
|
|
101
|
+
var memo = null;
|
|
102
|
+
if ( d.memo ) {
|
|
103
|
+
memo = providers.createProviderCache({ store: opts.store });
|
|
104
|
+
var kfs = providers.keyFromScope;
|
|
105
|
+
intakeFrag = memo.wrapFragment(intakeFrag, { 'Intake::type': kfs({ facts: ['rawText'], require: ['rawText'] }) });
|
|
106
|
+
reasonFrag = memo.wrapFragment(reasonFrag, {
|
|
107
|
+
'AI::evalComplexity': kfs({ facts: ['label', 'depth'], require: ['label'] }),
|
|
108
|
+
'AI::answer' : kfs({ facts: ['label'], require: ['label'] }),
|
|
109
|
+
'AI::confidence' : kfs({ facts: ['label', 'answer'], require: ['label'] }),
|
|
110
|
+
'AI::expand' : false, // structural (id-generating) — a cached template carries stale ids
|
|
111
|
+
'AI::rollup' : false, // fires once per task on the completion gate (memo gives nothing)
|
|
112
|
+
'AI::seedTask' : false, // structural + deterministic (no ask)
|
|
113
|
+
'AI::reportUp' : false // deterministic fan-in (no ask)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 5. wire the providers (process-global; one appliance per process — engine constraint) ─────
|
|
118
|
+
var frags = [intakeFrag, reasonFrag];
|
|
119
|
+
if ( verifyFrag ) frags.push(verifyFrag);
|
|
120
|
+
providers.register(Graph, frags);
|
|
121
|
+
|
|
122
|
+
// ── 6. the graph ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
var graph = new Graph({}, {
|
|
124
|
+
label: 'appliance', isMaster: true, autoMount: true, conceptSets: conceptSets,
|
|
125
|
+
bagRefManagers: {}, logLevel: d.logLevel, logger: opts.logger
|
|
126
|
+
}, conceptMap);
|
|
127
|
+
|
|
128
|
+
// ── the answer/refusal projection: PURE reads of discrete facts the bricks already emit ───────
|
|
129
|
+
// (keyed by the question's intake node id; the memo is CONTENT-addressed — rawText/label — so a
|
|
130
|
+
// fresh id per question still replays bit-identically on a repeat question.)
|
|
131
|
+
function project( qid ) {
|
|
132
|
+
var q = graph._objById[qid], task = graph._objById[qid + '_task'];
|
|
133
|
+
var qf = q && q._etty._;
|
|
134
|
+
if ( !qf ) return { status: 'refused', reason: 'untyped', missing: [], prose: null };
|
|
135
|
+
// intake-side refusal: a barrier miss never became a typed task.
|
|
136
|
+
if ( qf.IntakeStatus !== 'typed' ) {
|
|
137
|
+
return {
|
|
138
|
+
status : 'refused',
|
|
139
|
+
reason : qf.IntakeStatus || 'untyped', // 'untyped' | 'partial'
|
|
140
|
+
missing: Array.isArray(qf.IntakeMissing) ? qf.IntakeMissing.slice() : [],
|
|
141
|
+
prose : qf.intakeNarrative != null ? qf.intakeNarrative : null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
var tf = task && task._etty._;
|
|
145
|
+
// answer-side stuck: typed but no strategy routed it.
|
|
146
|
+
if ( !tf || tf.strategiesExhausted || tf.answer == null )
|
|
147
|
+
return { status: 'refused', reason: 'no-strategy', missing: [], prose: qf.intakeNarrative != null ? qf.intakeNarrative : null };
|
|
148
|
+
return { status: 'answered', answer: tf.answer, confBand: tf.confBand || null };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// is this question's sub-graph fully settled (answered, refused, or stuck)? → the fast-path replay.
|
|
152
|
+
function isResolved( qid ) {
|
|
153
|
+
var q = graph._objById[qid], qf = q && q._etty._;
|
|
154
|
+
if ( !qf || qf.IntakeStatus == null ) return false;
|
|
155
|
+
if ( qf.IntakeStatus !== 'typed' ) return true; // an intake-side refusal is terminal
|
|
156
|
+
var t = graph._objById[qid + '_task'], tf = t && t._etty._;
|
|
157
|
+
return !!(tf && (tf.answer != null || tf.strategiesExhausted));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
graph: graph,
|
|
162
|
+
memo : memo, // expose the cache (stats/store) — P1.c reads memo.stats for the G4 replay gate
|
|
163
|
+
settle: function () { return nextStable(graph); },
|
|
164
|
+
close: function () { if ( graph && graph.destroy ) graph.destroy(); },
|
|
165
|
+
|
|
166
|
+
/** answer a question → a typed answer or a typed refusal (see Result above). */
|
|
167
|
+
answer: function ( question, aopts ) {
|
|
168
|
+
aopts = aopts || {};
|
|
169
|
+
var qid = qidFor(question);
|
|
170
|
+
// fast path: this exact question is already resolved in the graph → replay it, 0 model calls.
|
|
171
|
+
if ( isResolved(qid) ) return Promise.resolve(project(qid));
|
|
172
|
+
return new Promise(function ( resolve, reject ) {
|
|
173
|
+
var to = aopts.timeout || 120000;
|
|
174
|
+
var timer = setTimeout(function () { graph.un('stabilize', done); reject(new Error('appliance.answer timed out after ' + to + 'ms')); }, to);
|
|
175
|
+
var settled = false;
|
|
176
|
+
function done() {
|
|
177
|
+
if ( settled || !isResolved(qid) ) return;
|
|
178
|
+
settled = true; clearTimeout(timer); graph.un('stabilize', done); resolve(project(qid));
|
|
179
|
+
}
|
|
180
|
+
graph.on('stabilize', done);
|
|
181
|
+
try {
|
|
182
|
+
graph.pushMutation({ $$_id: qid, Node: true, rawText: String(question) }, null);
|
|
183
|
+
} catch ( e ) { clearTimeout(timer); graph.un('stabilize', done); reject(e); }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { createAppliance: createAppliance };
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* createCriticalMind — the external critical mind as a combo (C9): split a user question into
|
|
4
|
+
* declared viewpoints, ESTABLISH each one against a statement pool through a witness gate
|
|
5
|
+
* (cap-2 precision, round 1 on the full pool + open points retried on the stance slice — the
|
|
6
|
+
* measured v1.1 ladder), GENERATE the missing theses anchored-by-witnesses (clusters + quarantine +
|
|
7
|
+
* semantic SAME/NEW gate + fuse-to-open), keep everything in a typed LEDGER, and render a
|
|
8
|
+
* certification-aware verdict. An unforced brainstorm builds the FREE pool in TWO steps: list
|
|
9
|
+
* statements unlabeled, then label each one by a single forced choice (PRO|CON|OFF-TOPIC):
|
|
10
|
+
*
|
|
11
|
+
* frame status FREE (model-brainstormed pool) / MATERIAL (caller statements) / DECLARED
|
|
12
|
+
* (caller viewpoints) — a MECHANICAL verdict fires only at count margin ≥ 3 (the measured
|
|
13
|
+
* decidability bound; a certified .sgc perimeter is what buys margin ≥ 2 — measured 24/24);
|
|
14
|
+
* below the bound the honest output is counts + coverage + UNDECIDED, never a fake verdict.
|
|
15
|
+
*
|
|
16
|
+
* Assembles the measured FULL config of the generative critical loop (WIP campaigns 07-12/07-13:
|
|
17
|
+
* decision ladder by margin · cluster-anchored generation · quarantine · SAME/NEW semantic gate ·
|
|
18
|
+
* fuse witness-transfer to open declared points · 0-fabrication held across all negative
|
|
19
|
+
* controls). The LEDGER is the deliverable; `renderProse` turns it into a readable report and
|
|
20
|
+
* `polish: true` adds ONE bounded rewrite pass constrained to the given content (presentation
|
|
21
|
+
* only — the ledger stays the audit trail).
|
|
22
|
+
*
|
|
23
|
+
* RE-ROOT (v3, the harness cycle wired in — WIP boucle-generative): after exploring the declared
|
|
24
|
+
* perimeter, IF the node is UNCERTAIN (G3 placement: margin below THIS frame's decidability
|
|
25
|
+
* threshold OR a declared point still open — transposed to C9's per-frame threshold so a margin-2
|
|
26
|
+
* node a verdict needs is never left ungenerated) it runs ONE proven generation pass, then
|
|
27
|
+
* RECONCILES the ledger (JTMS: an entry whose witnesses left the pool is retracted, the uniform
|
|
28
|
+
* in-pool test cascading shared support) and decides on the enriched perimeter. `result.rounds` =
|
|
29
|
+
* 1 if the re-root generated, else 0; `result.journal` = the typed delta log. Generation is a
|
|
30
|
+
* SINGLE pass by design — a second generation round inflates the count on an uncertified
|
|
31
|
+
* FREE/MATERIAL frame past the decidability bound by mining majority coverage (the illusion the
|
|
32
|
+
* certified perimeter guards against). The full ledger-driven re-SPLIT (new viewpoints from the
|
|
33
|
+
* generated vocabulary — round 2's distinctive work) awaits the real lattice (P2), out of scope
|
|
34
|
+
* here; this wires the reconcile + re-decide + placement the harness proved, no more.
|
|
35
|
+
*
|
|
36
|
+
* DIALECTIC (v3, opt-in `dialectic: true`) — on a contested node (verdict UNDECIDED), anchored
|
|
37
|
+
* cross-refutation surfaces each established point's OPPOSITE-side attackers (`attackers`) and marks
|
|
38
|
+
* it `contested` (partial validity, the KP-history). It ENRICHES the audit, it does NOT decide: the
|
|
39
|
+
* counts/verdict are never moved (annotation only — the anti-illusion invariant). Proven in harness.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const SYSTEM = 'You are a careful analyst. Follow the output format EXACTLY.';
|
|
43
|
+
const GEN_TRIES = 3;
|
|
44
|
+
const K_RETRIES = 2; // v1.1 explore retries (open points → stance slice)
|
|
45
|
+
const K_ROUNDS = 2; // re-root bound (K=2, anti trou-sans-fond) — RESERVED for the future ledger-driven re-SPLIT loop (P2); today the re-root is a single proven generation pass (a 2nd generation round inflates uncertified margins — see run())
|
|
46
|
+
const LIST_TRIES = 2; // brainstorm list step: initial call + ONE bounded "list MORE" re-ask
|
|
47
|
+
const MECHANICAL_MARGIN = { FREE: 3, MATERIAL: 3, DECLARED: 3, STOCK: 2 }; // the measured bound
|
|
48
|
+
|
|
49
|
+
const parseCites = ( raw ) => /cites?:\s*NONE/i.test(String(raw || '')) ? [] : (String(raw || '').match(/[pc]\d+/g) || null);
|
|
50
|
+
const poolLines = ( pool ) => pool.map(( a ) => a.id + ': ' + a.text ).join('\n');
|
|
51
|
+
const activeCounts = ( ledger ) => { const c = { PRO: 0, CON: 0 }; for ( const e of ledger ) if ( e.status === 'active' && e.witnesses ) c[e.side]++; return c; };
|
|
52
|
+
|
|
53
|
+
// RE-ROOT reconciliation (JTMS, the harness re-root step + the owner SUIVI/RÉCONCILIATION requirement):
|
|
54
|
+
// an active entry whose witnesses no longer ALL live in the pool is RETRACTED. The uniform in-pool test
|
|
55
|
+
// gives the cascade for free — if a witness leaves the pool, EVERY entry citing it fails `every(inPool)`
|
|
56
|
+
// and falls together (shared-support withdrawal), while an entry retracted for one bad witness never
|
|
57
|
+
// drags down entries sharing its still-valid witnesses. On C9's happy path all witnesses are in-pool by
|
|
58
|
+
// construction, so this is a no-op; it is the load-bearing guard for injected/stale ledger entries (the
|
|
59
|
+
// NEG-ledger control) and the prerequisite for a future ledger-driven re-split (P2). Pure + structural.
|
|
60
|
+
function reconcile( ledger, pool, journal ) {
|
|
61
|
+
const inPool = new Set(pool.map(( a ) => a.id ));
|
|
62
|
+
const retracted = [];
|
|
63
|
+
for ( const e of ledger )
|
|
64
|
+
if ( e.status === 'active' && e.witnesses && !e.witnesses.every(( w ) => inPool.has(w) ) ) {
|
|
65
|
+
e.status = 'retracted'; retracted.push(e.key);
|
|
66
|
+
if ( journal ) journal.push('reconcile: RETRACTED ' + e.key + ' (witness left the pool)');
|
|
67
|
+
}
|
|
68
|
+
return retracted;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createCriticalMind( opts ) {
|
|
72
|
+
const ask = opts.ask;
|
|
73
|
+
if ( typeof ask !== 'function' ) throw new Error('createCriticalMind: opts.ask (async {system,user,maxTokens,temperature} → text) is required');
|
|
74
|
+
const onStage = typeof opts.onStage === 'function' ? opts.onStage : () => {};
|
|
75
|
+
|
|
76
|
+
async function buildPool( topic, statements ) {
|
|
77
|
+
if ( Array.isArray(statements) && statements.length ) {
|
|
78
|
+
const pool = []; let p = 0, c = 0, dropped = 0;
|
|
79
|
+
for ( const s of statements ) {
|
|
80
|
+
const rec = typeof s === 'string' ? (s.match(/^(PRO|CON)\s*:\s*(.+)$/i) || null) : null;
|
|
81
|
+
const side = rec ? rec[1].toUpperCase() : (s && s.side ? String(s.side).toUpperCase() : null);
|
|
82
|
+
const text = rec ? rec[2].trim() : (s && s.text ? String(s.text).trim() : null);
|
|
83
|
+
if ( (side !== 'PRO' && side !== 'CON') || !text ) { dropped++; continue; }
|
|
84
|
+
pool.push({ id: side === 'PRO' ? 'p' + (++p) : 'c' + (++c), side, text });
|
|
85
|
+
}
|
|
86
|
+
if ( dropped ) onStage('POOL', dropped + ' statements dropped (each needs side PRO|CON + text, or a "PRO: ..." line)');
|
|
87
|
+
return { pool, status: 'MATERIAL' };
|
|
88
|
+
}
|
|
89
|
+
// UNFORCED brainstorm, TWO-STEP. Asking the model to list AND label in one pass came back
|
|
90
|
+
// near-empty and mislabeled on the low-quant ("PRO: no framework supports..." is a CON);
|
|
91
|
+
// listing WITHOUT labels first, then labeling each statement by one FORCED CHOICE, is the
|
|
92
|
+
// closed-competition structure that discriminates (yes/no acquiesces — measured, 5×).
|
|
93
|
+
// The pool's PRO/CON distribution must reflect the question, not a forced 10-vs-10 symmetry
|
|
94
|
+
// (lopsided questions must be allowed to produce lopsided pools — the count is the signal).
|
|
95
|
+
// The low-quant often stops the list after 2-3 lines (measured live on a FR topic → pool
|
|
96
|
+
// below the 4-statement floor). Bounded re-ask (the minSteps idiom): ONE "list MORE" retry
|
|
97
|
+
// while under the floor + a margin for forced-choice drops; it never asks to balance sides.
|
|
98
|
+
let cands = [];
|
|
99
|
+
for ( let t = 0; t < LIST_TRIES && cands.length < 6; t++ ) {
|
|
100
|
+
const out = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
101
|
+
+ '\nList the strongest DISTINCT statements informed people ACTUALLY make about this question, whichever side (one concrete argument each, up to 20 lines).'
|
|
102
|
+
+ '\nIf one side has far fewer serious statements, reflect that honestly — do NOT balance the sides artificially.'
|
|
103
|
+
+ (cands.length ? '\nAlready listed (do NOT repeat these):\n' + cands.map(( c ) => '- ' + c ).join('\n')
|
|
104
|
+
+ '\nList UP TO 10 MORE distinct statements, or reply exactly NONE if there are no more serious ones.' : '')
|
|
105
|
+
+ '\nReply one per line, each: S: <one sentence>', maxTokens: 700, temperature: 0 }));
|
|
106
|
+
if ( /^\s*NONE\s*$/i.test(out.trim()) ) break;
|
|
107
|
+
const got = (out.match(/S:\s*[^\n]+/g) || []).map(( l ) => l.replace(/^S:\s*/, '').trim() ).filter(( s ) => s.length >= 5 );
|
|
108
|
+
const before = cands.length;
|
|
109
|
+
for ( const g of got ) if ( !cands.some(( c ) => c.toLowerCase() === g.toLowerCase() ) ) cands.push(g);
|
|
110
|
+
if ( cands.length === before ) break; // no progress → a same-shape retry is a no-op
|
|
111
|
+
}
|
|
112
|
+
cands = cands.slice(0, 24);
|
|
113
|
+
const pool = []; let p = 0, c = 0, off = 0;
|
|
114
|
+
for ( const text of cands ) {
|
|
115
|
+
const lab = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
116
|
+
+ '\nStatement: ' + text
|
|
117
|
+
+ '\nIn the context of the question, does this statement support answering YES (PRO), support answering NO (CON), or is it OFF-TOPIC?'
|
|
118
|
+
+ '\nReply ONLY one of: PRO | CON | OFF-TOPIC', maxTokens: 8, temperature: 0 }));
|
|
119
|
+
const m = lab.match(/\b(OFF[- ]?TOPIC|PRO|CON)\b/i);
|
|
120
|
+
const side = m && !/OFF/i.test(m[1]) ? m[1].toUpperCase() : null;
|
|
121
|
+
if ( side && (side === 'PRO' ? p : c) < 12 ) pool.push({ id: side === 'PRO' ? 'p' + (++p) : 'c' + (++c), side, text });
|
|
122
|
+
else off++;
|
|
123
|
+
}
|
|
124
|
+
if ( off ) onStage('POOL', off + ' brainstormed statements dropped by the forced-choice label (OFF-TOPIC or unparseable)');
|
|
125
|
+
return { pool, status: 'FREE' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function declareViewpoints( topic, pool, given ) {
|
|
129
|
+
if ( Array.isArray(given) && given.length )
|
|
130
|
+
return given.map(( s, i ) => ({ key: 'V' + (i + 1), side: (s && s.side) ? String(s.side).toUpperCase() : null,
|
|
131
|
+
text: typeof s === 'string' ? s.trim() : String(s.text || '').trim() }) ).filter(( v ) => v.text );
|
|
132
|
+
const vps = [];
|
|
133
|
+
for ( const side of ['PRO', 'CON'] ) {
|
|
134
|
+
const slice = pool.filter(( a ) => a.side === side );
|
|
135
|
+
if ( !slice.length ) continue;
|
|
136
|
+
const out = String(await ask({ system: SYSTEM, user: 'Question: ' + topic + '\n' + side + ' statements:\n' + poolLines(slice)
|
|
137
|
+
+ '\nName the 2 main DISTINCT points of view these statements support. Reply 2 lines, each: V: <short name of the viewpoint>', maxTokens: 80, temperature: 0 }));
|
|
138
|
+
(out.match(/V:\s*[^\n]+/g) || []).slice(0, 2).forEach(( l ) =>
|
|
139
|
+
vps.push({ key: 'V' + (vps.length + 1), side, text: l.replace(/^V:\s*/, '').trim() }) );
|
|
140
|
+
}
|
|
141
|
+
return vps;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The measured v1.1 ladder (+14 pts coverage at P=24): round 1 explores every viewpoint against
|
|
145
|
+
// the FULL pool; the points left OPEN are retried on their STANCE SLICE — a structural narrowing
|
|
146
|
+
// (stance = input metadata, never a model output). A same-prompt retry is a no-op at temp 0
|
|
147
|
+
// (measured), so a retry without a narrower slice is skipped, not repeated.
|
|
148
|
+
async function explore( topic, pool, vps, ledger, journal ) {
|
|
149
|
+
const leaf = async ( v, slice ) => {
|
|
150
|
+
const out = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
151
|
+
+ '\nPoint of view' + (v.side ? ' (' + v.side + ')' : '') + ': ' + v.text
|
|
152
|
+
+ '\nStatements:\n' + poolLines(slice)
|
|
153
|
+
+ '\nWhich statements GENUINELY make this exact point (not merely the same side)? Cite AT MOST 2. Reply ONE line, format: cites: ids_or_NONE', maxTokens: 48, temperature: 0 }));
|
|
154
|
+
// stance gate on cites (metadata): an opposite-side cite is dropped, never admitted
|
|
155
|
+
return (parseCites(out) || []).filter(( x ) => slice.some(( a ) => a.id === x && (!v.side || a.side === v.side) ) );
|
|
156
|
+
};
|
|
157
|
+
const establish = ( entry, ids, how ) => {
|
|
158
|
+
entry.witnesses = ids.slice(0, 2); entry.status = 'active';
|
|
159
|
+
if ( !entry.side ) entry.side = pool.find(( a ) => a.id === ids[0] ).side;
|
|
160
|
+
onStage('EXPLORE', entry.key + ' → established (' + entry.witnesses.join('+') + how + ')');
|
|
161
|
+
journal.push('R0 established ' + entry.key + ' (' + entry.side + ', ' + entry.witnesses.join('+') + how + ')');
|
|
162
|
+
};
|
|
163
|
+
let pending = [];
|
|
164
|
+
for ( const v of vps ) {
|
|
165
|
+
const entry = { key: v.key, kind: 'declared', side: v.side || null, text: v.text, witnesses: null, status: 'open', round: 0, provenance: 'declared' };
|
|
166
|
+
ledger.push(entry);
|
|
167
|
+
const ids = await leaf(v, pool);
|
|
168
|
+
if ( ids.length ) establish(entry, ids, '');
|
|
169
|
+
else pending.push({ v, entry });
|
|
170
|
+
}
|
|
171
|
+
for ( let r = 0; r < K_RETRIES && pending.length; r++ ) {
|
|
172
|
+
const next = [];
|
|
173
|
+
for ( const { v, entry } of pending ) {
|
|
174
|
+
const slice = v.side ? pool.filter(( a ) => a.side === v.side ) : [];
|
|
175
|
+
if ( !slice.length || slice.length === pool.length ) { onStage('EXPLORE', entry.key + ' → OPEN (no narrower slice to retry on)'); continue; }
|
|
176
|
+
const ids = await leaf(v, slice);
|
|
177
|
+
if ( ids.length ) establish(entry, ids, ', retry ' + (r + 1));
|
|
178
|
+
else next.push({ v, entry });
|
|
179
|
+
}
|
|
180
|
+
pending = next;
|
|
181
|
+
}
|
|
182
|
+
for ( const { entry } of pending ) { onStage('EXPLORE', entry.key + ' → OPEN'); journal.push('R0 open ' + entry.key + ' (' + (entry.side || '?') + ', no valid witnesses)'); }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function generate( topic, pool, ledger, round, journal ) {
|
|
186
|
+
const usedArgs = new Set(ledger.flatMap(( e ) => e.witnesses || [] ));
|
|
187
|
+
for ( const side of ['PRO', 'CON'] ) {
|
|
188
|
+
const tried = new Set();
|
|
189
|
+
for ( let i = 0; i < GEN_TRIES; i++ ) {
|
|
190
|
+
const unused = pool.filter(( a ) => a.side === side && !usedArgs.has(a.id) && !tried.has(a.id) );
|
|
191
|
+
if ( unused.length < 2 ) break;
|
|
192
|
+
const seed = unused[0]; tried.add(seed.id);
|
|
193
|
+
const others = unused.filter(( a ) => a.id !== seed.id );
|
|
194
|
+
const cOut = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
195
|
+
+ '\nCandidate point (' + side + '): ' + seed.text
|
|
196
|
+
+ '\nStatements:\n' + poolLines(others)
|
|
197
|
+
+ '\nWhich statements GENUINELY make this exact point (not merely the same side)? Cite AT MOST 2. Reply ONE line, format: cites: ids_or_NONE', maxTokens: 48, temperature: 0 }));
|
|
198
|
+
const cIds = (parseCites(cOut) || []).filter(( x ) => others.some(( a ) => a.id === x ) );
|
|
199
|
+
const slate = cIds.length ? unused.filter(( a ) => a.id === seed.id || cIds.includes(a.id) ) : unused;
|
|
200
|
+
const table = ledger.filter(( e ) => e.side === side && (e.kind === 'declared' || e.status === 'active') ).map(( e ) => '- ' + e.text.slice(0, 90) );
|
|
201
|
+
const out = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
202
|
+
+ '\nKnown ' + side + ' points (already on the table):\n' + table.join('\n')
|
|
203
|
+
+ '\nUNUSED statements:\n' + poolLines(slate)
|
|
204
|
+
+ '\nPropose ONE NEW ' + side + ' point of view, DIFFERENT from the known points, that at least TWO of these unused statements genuinely make. '
|
|
205
|
+
+ '\nReply ONE line, format: THESIS: <one short sentence> | cites: id, id (or exactly: NONE)', maxTokens: 80, temperature: 0 }));
|
|
206
|
+
(String(out).match(/[pc]\d+/g) || []).filter(( x ) => pool.some(( a ) => a.id === x ) ).forEach(( x ) => tried.add(x) );
|
|
207
|
+
const srcLine = [...String(out).split(/\n/).map(( l ) => l.trim() ).filter(Boolean)].reverse().find(( l ) => /THESIS:/i.test(l) || /^NONE$/i.test(l) ) || String(out);
|
|
208
|
+
if ( /^\s*NONE\s*$/i.test(srcLine) ) { onStage('GEN', side + ': honest NONE'); break; }
|
|
209
|
+
const mT = srcLine.match(/THESIS:\s*([^|]{5,200})\|/i);
|
|
210
|
+
const ids = [...new Set(parseCites(srcLine) || [])].filter(( x ) => pool.some(( a ) => a.id === x && a.side === side ) && !usedArgs.has(x) );
|
|
211
|
+
if ( !mT || ids.length < 2 ) { onStage('GEN', side + ': gate refusal (needs ≥2 unused in-pool witnesses)'); continue; }
|
|
212
|
+
const overlap = ledger.find(( e ) => e.status === 'active' && e.witnesses && ids.filter(( x ) => e.witnesses.includes(x) ).length / ids.length >= 0.5 );
|
|
213
|
+
if ( overlap ) { onStage('GEN', side + ': merged into ' + overlap.key); continue; }
|
|
214
|
+
const declared = ledger.filter(( e ) => e.kind === 'declared' );
|
|
215
|
+
const cls = String(await ask({ system: SYSTEM, user: 'Known points:\n' + declared.map(( e, di ) => (di + 1) + '. ' + e.text.slice(0, 90) ).join('\n')
|
|
216
|
+
+ '\nCandidate thesis: ' + mT[1].trim()
|
|
217
|
+
+ '\nIs the candidate just a restatement of one known point?\nReply with ONLY: SAME <number> or NEW.', maxTokens: 8, temperature: 0 }));
|
|
218
|
+
const mS = cls.match(/SAME\s*(\d)/i);
|
|
219
|
+
if ( mS ) {
|
|
220
|
+
const target = declared[Number(mS[1]) - 1];
|
|
221
|
+
if ( target && target.status === 'open' && target.side === side ) { // fuse: redundancy TRANSPORTS coverage
|
|
222
|
+
target.witnesses = ids.slice(0, 2); target.status = 'active';
|
|
223
|
+
ids.forEach(( x ) => usedArgs.add(x) );
|
|
224
|
+
onStage('GEN', side + ': fuse — ' + target.key + ' open→established');
|
|
225
|
+
journal.push('R' + round + ' fuse ' + target.key + ' open→established (' + target.witnesses.join('+') + ')');
|
|
226
|
+
} else onStage('GEN', side + ': semantic SAME as ' + (target ? target.key : '?') + ' — dropped');
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const key = 'G' + (ledger.filter(( e ) => e.kind === 'generated' ).length + 1);
|
|
230
|
+
ledger.push({ key, kind: 'generated', side, text: mT[1].trim(), witnesses: ids.slice(0, 3), status: 'active', round, provenance: 'generated+witnesses' });
|
|
231
|
+
ids.forEach(( x ) => usedArgs.add(x) );
|
|
232
|
+
onStage('GEN', side + ': NEW ' + key + ' (' + ids.slice(0, 3).join('+') + ')');
|
|
233
|
+
journal.push('R' + round + ' gen-NEW ' + key + ' (' + side + ', ' + ids.slice(0, 3).join('+') + ')');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// DIALECTIC (v3, opt-in `dialectic: true`) — anchored cross-refutation on a CONTESTED node (verdict
|
|
239
|
+
// UNDECIDED): for each established viewpoint, the OPPOSITE-side statements that SPECIFICALLY contradict
|
|
240
|
+
// it become anchored `attackers` (the same classification-on-given-text family as explore, stance-gated
|
|
241
|
+
// — never the refuted Q2 auto-audit). An attacked point stays ACTIVE but `contested` (partial validity
|
|
242
|
+
// — the KP-history): the counts/verdict are NEVER moved (annotation only, the anti-illusion invariant);
|
|
243
|
+
// the synthesis renders it as diminished. Proven in harness (WIP 2026-07-13-c9v3-dialectique: anchoring
|
|
244
|
+
// 100%, 0 decision moved, NEG menteur/brouille clean). Placement: contested nodes only (the CQ finding).
|
|
245
|
+
async function crossRefute( topic, pool, ledger ) {
|
|
246
|
+
for ( const e of ledger.filter(( x ) => x.status === 'active' && x.witnesses ) ) {
|
|
247
|
+
const oppSide = e.side === 'PRO' ? 'CON' : 'PRO';
|
|
248
|
+
const slice = pool.filter(( a ) => a.side === oppSide );
|
|
249
|
+
if ( !slice.length ) continue;
|
|
250
|
+
const out = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
251
|
+
+ '\nEstablished point (' + e.side + '): ' + e.text
|
|
252
|
+
+ '\nOpposing statements:\n' + poolLines(slice)
|
|
253
|
+
+ '\nWhich of these statements SPECIFICALLY CONTRADICT the established point (make it false or insufficient), not merely argue the other side? Cite AT MOST 2. Reply ONE line, format: cites: ids_or_NONE', maxTokens: 48, temperature: 0 }));
|
|
254
|
+
// stance + in-pool gate on the attackers; never the point's own witnesses
|
|
255
|
+
const att = (parseCites(out) || []).filter(( x ) => slice.some(( a ) => a.id === x ) && !(e.witnesses || []).includes(x) );
|
|
256
|
+
if ( att.length ) { e.attackers = att.slice(0, 2); e.contested = true; onStage('DIALECTIC', e.key + ' contested (attacked by ' + e.attackers.join('+') + ')'); }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** deterministic prose from the LEDGER — the audit trail stays the source of truth. */
|
|
261
|
+
function renderProse( r ) {
|
|
262
|
+
const quote = ( id ) => { const a = r.pool.find(( x ) => x.id === id ); return a ? '“' + a.text + '”' : id; };
|
|
263
|
+
const sideBlock = ( side, title ) => {
|
|
264
|
+
const items = r.ledger.filter(( e ) => e.status === 'active' && e.witnesses && e.side === side );
|
|
265
|
+
if ( !items.length ) return '## ' + title + '\n\nNothing could be established on this side from the pool.\n';
|
|
266
|
+
let s = '## ' + title + '\n\n' + (r.synthesis[side] ? r.synthesis[side] + '\n' : '');
|
|
267
|
+
for ( const e of items ) s += '\n- **' + e.text + '**' + (e.kind === 'generated' ? ' *(new angle found while reading)*' : '')
|
|
268
|
+
+ (e.contested ? ' *(contested — attacked by ' + e.attackers.map(quote).join(' · ') + ')*' : '')
|
|
269
|
+
+ '\n ' + e.witnesses.map(quote).join(' · ') + '\n';
|
|
270
|
+
return s;
|
|
271
|
+
};
|
|
272
|
+
const open = r.ledger.filter(( e ) => e.status === 'open' );
|
|
273
|
+
let s = '# ' + r.topic + '\n\n'
|
|
274
|
+
+ '*Frame status: **' + r.frameStatus + '**'
|
|
275
|
+
+ (r.frameStatus === 'FREE' ? ' — the statement pool is model-generated; coverage is relative to this pool, not the world.' : '')
|
|
276
|
+
+ (r.frameStatus === 'MATERIAL' ? ' — statements supplied by the caller; the decision frame is not certified.' : '')
|
|
277
|
+
+ (r.frameStatus === 'DECLARED' ? ' — viewpoints declared by the caller.' : '') + '*\n\n'
|
|
278
|
+
+ sideBlock('PRO', 'The case for') + '\n' + sideBlock('CON', 'The case against') + '\n';
|
|
279
|
+
if ( open.length ) s += '## Points that could not be established\n\n' + open.map(( e ) => '- ' + e.text + ' *(no valid witnesses — left open, not faked)*' ).join('\n') + '\n\n';
|
|
280
|
+
s += '## Bottom line\n\n';
|
|
281
|
+
if ( r.basis === 'mechanical-count' )
|
|
282
|
+
s += '**' + r.verdict + '** — anchored points ' + r.counts.PRO + ' vs ' + r.counts.CON + ' (margin ' + r.margin + ' ≥ ' + r.threshold + '): the verdict is mechanical, no model weighing involved.\n';
|
|
283
|
+
else if ( r.basis === 'settled-norm' )
|
|
284
|
+
s += '**' + r.verdict + '** — *on the basis of a settled norm, not of the counts* (anchored points ' + r.counts.PRO + ' vs ' + r.counts.CON
|
|
285
|
+
+ ', margin ' + r.margin + ' below the bound): the contestedness probe finds this question settled among informed people. '
|
|
286
|
+
+ 'This basis is an ADVISORY prior — announced, not a gated fact; the anchored coverage above is the audited part.\n';
|
|
287
|
+
else if ( r.basis === 'norm-vs-counts-tension' )
|
|
288
|
+
s += '**UNDECIDED, with a tension worth knowing**: the contestedness probe says the question is settled toward ' + (r.norm && r.norm.side)
|
|
289
|
+
+ ', but the anchored counts lean the other way (PRO ' + r.counts.PRO + ' vs CON ' + r.counts.CON + '). '
|
|
290
|
+
+ 'Neither signal overrides the other here — inspect the ledger, or supply material.\n';
|
|
291
|
+
else
|
|
292
|
+
s += 'Anchored points: PRO ' + r.counts.PRO + ' vs CON ' + r.counts.CON + ' (margin ' + r.margin + ')'
|
|
293
|
+
+ (r.norm && r.norm.status === 'CONTESTED' ? ', and the contestedness probe confirms the question is GENUINELY contested' : '')
|
|
294
|
+
+ '. On a ' + r.frameStatus + ' frame this margin is below the measured decidability bound (' + r.threshold + '), so no verdict is rendered — '
|
|
295
|
+
+ 'the honest deliverable is the anchored coverage above. A certified perimeter (.sgc stock) is what buys a verdict at margin ≥ 2.\n';
|
|
296
|
+
return s;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function run( input ) {
|
|
300
|
+
const topic = String(input && input.topic || '').trim();
|
|
301
|
+
if ( !topic ) throw new Error('critique: input.topic is required');
|
|
302
|
+
const ledger = [], journal = [];
|
|
303
|
+
const { pool, status: poolStatus } = await buildPool(topic, input.statements);
|
|
304
|
+
const nPro = pool.filter(( a ) => a.side === 'PRO' ).length, nCon = pool.filter(( a ) => a.side === 'CON' ).length;
|
|
305
|
+
// a LOPSIDED pool is not an error — it is the signal (an unforced brainstorm on a question
|
|
306
|
+
// nobody seriously argues one side of SHOULD come back one-sided; the counts then separate
|
|
307
|
+
// mechanically). Refuse only when there is nothing to anchor on at all.
|
|
308
|
+
if ( pool.length < 4 || Math.max(nPro, nCon) < 2 )
|
|
309
|
+
return { topic, frameStatus: poolStatus, error: 'pool too small (need ≥4 statements with ≥2 on at least one side)', pool, ledger, verdict: 'UNDECIDED' };
|
|
310
|
+
if ( Math.min(nPro, nCon) < 2 ) onStage('POOL', 'one-sided pool (' + nPro + ' PRO / ' + nCon + ' CON) — announced, and itself a signal');
|
|
311
|
+
const frameStatus = (Array.isArray(input.viewpoints) && input.viewpoints.length) ? 'DECLARED' : poolStatus;
|
|
312
|
+
onStage('POOL', pool.length + ' statements · status ' + poolStatus);
|
|
313
|
+
const vps = await declareViewpoints(topic, pool, input.viewpoints);
|
|
314
|
+
onStage('SPLIT', vps.length + ' declared viewpoints · frame ' + frameStatus);
|
|
315
|
+
const threshold = MECHANICAL_MARGIN[frameStatus] || 3;
|
|
316
|
+
await explore(topic, pool, vps, ledger, journal);
|
|
317
|
+
// RE-ROOT (the harness cycle wired in): generate ONE proven pass (GEN_TRIES per side) IF the
|
|
318
|
+
// node is UNCERTAIN — G3 placement: margin below THIS frame's decidability threshold OR a
|
|
319
|
+
// declared point still open (transposed to C9's per-frame threshold so a margin-2 node a
|
|
320
|
+
// verdict needs is never left ungenerated) — then RECONCILE the ledger (JTMS: an entry whose
|
|
321
|
+
// witnesses left the pool is retracted, cascading shared support) and decide on the enriched
|
|
322
|
+
// perimeter. Generation is a SINGLE pass on purpose: a second generation round changes the
|
|
323
|
+
// measured budget and, on an uncertified FREE/MATERIAL frame, pushes the count past the
|
|
324
|
+
// decidability bound by mining MAJORITY coverage rather than by genuine decidability — the
|
|
325
|
+
// illusion the certified perimeter guards against (consigned finding, measured live on the
|
|
326
|
+
// meta topic: a 2nd round flipped an honest UNDECIDED to a mechanical verdict). The full
|
|
327
|
+
// ledger-driven re-SPLIT (round 2's distinctive work — new viewpoints from the generated
|
|
328
|
+
// vocabulary) needs the real lattice (P2); K_ROUNDS reserves that bound for the future loop.
|
|
329
|
+
const c0 = activeCounts(ledger);
|
|
330
|
+
let rounds = 0;
|
|
331
|
+
if ( Math.abs(c0.PRO - c0.CON) < threshold || ledger.some(( e ) => e.status === 'open' ) ) {
|
|
332
|
+
await generate(topic, pool, ledger, 1, journal);
|
|
333
|
+
reconcile(ledger, pool, journal);
|
|
334
|
+
rounds = 1;
|
|
335
|
+
}
|
|
336
|
+
const counts = activeCounts(ledger);
|
|
337
|
+
const margin = Math.abs(counts.PRO - counts.CON);
|
|
338
|
+
let verdict = margin >= threshold ? (counts.PRO > counts.CON ? 'PRO' : 'CON') : 'UNDECIDED';
|
|
339
|
+
let basis = verdict === 'UNDECIDED' ? null : 'mechanical-count';
|
|
340
|
+
// CONTESTEDNESS probe (SOFT-lane semantics: an ADVISORY prior, announced as such, never a gated
|
|
341
|
+
// fact) — one FORCED-CHOICE call (the closed-competition structure that discriminates; yes/no
|
|
342
|
+
// acquiesces — measured). It separates "genuinely contested at this margin" (honest UNDECIDED)
|
|
343
|
+
// from "settled by a broad norm" (a question that was never really open): without it, a
|
|
344
|
+
// symmetric-ish pool makes UNDECIDED the answer to everything.
|
|
345
|
+
let norm = null;
|
|
346
|
+
if ( verdict === 'UNDECIDED' ) {
|
|
347
|
+
const nOut = String(await ask({ system: SYSTEM, user: 'Question: ' + topic
|
|
348
|
+
+ '\nAmong informed people, is this question genuinely CONTESTED, or is there a SETTLED answer (law, widely shared ethics, broad consensus)?'
|
|
349
|
+
+ '\nReply ONLY one of: SETTLED PRO | SETTLED CON | CONTESTED', maxTokens: 8, temperature: 0 }));
|
|
350
|
+
const mN = nOut.match(/SETTLED\s*(PRO|CON)/i);
|
|
351
|
+
norm = mN ? { status: 'SETTLED', side: mN[1].toUpperCase() } : { status: 'CONTESTED' };
|
|
352
|
+
const lean = counts.PRO > counts.CON ? 'PRO' : counts.CON > counts.PRO ? 'CON' : null;
|
|
353
|
+
if ( norm.status === 'SETTLED' && (!lean || lean === norm.side) ) { verdict = norm.side; basis = 'settled-norm'; }
|
|
354
|
+
else if ( norm.status === 'SETTLED' ) basis = 'norm-vs-counts-tension'; // reported, never papered over
|
|
355
|
+
}
|
|
356
|
+
if ( input.dialectic && verdict === 'UNDECIDED' ) await crossRefute(topic, pool, ledger); // enrich the contested node — annotation only, never moves the counts
|
|
357
|
+
const synthesis = {};
|
|
358
|
+
for ( const side of ['PRO', 'CON'] ) {
|
|
359
|
+
const items = ledger.filter(( e ) => e.status === 'active' && e.witnesses && e.side === side );
|
|
360
|
+
if ( !items.length ) continue;
|
|
361
|
+
synthesis[side] = String(await ask({ system: SYSTEM, user: 'Question: ' + topic + '\nEstablished ' + side + ' points:\n'
|
|
362
|
+
+ items.map(( c ) => '- ' + c.text.slice(0, 90) ).join('\n') + '\nSummarize the ' + side + ' case in ONE line (no ids).', maxTokens: 60, temperature: 0 })).trim();
|
|
363
|
+
}
|
|
364
|
+
const result = { topic, frameStatus, rounds, journal, pool, viewpoints: vps, ledger, counts, margin, threshold, verdict, basis, norm, synthesis };
|
|
365
|
+
result.prose = renderProse(result);
|
|
366
|
+
if ( input.polish ) { // presentation-only rewrite, content-locked
|
|
367
|
+
const polished = String(await ask({ system: 'You are an editor. Rewrite the report below into flowing prose. Use ONLY the content provided. Do NOT add facts, numbers, or claims. Keep the frame-status caveat and the bottom line verbatim in meaning.',
|
|
368
|
+
user: result.prose, maxTokens: 700, temperature: 0 }));
|
|
369
|
+
result.polished = polished.trim();
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { run, renderProse };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = { createCriticalMind, reconcile };
|