phaedo-mcp 0.2.1 → 0.3.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/NOTICE +17 -0
- package/README.md +51 -9
- package/action-binding.js +82 -0
- package/agreement.js +38 -0
- package/ap2.js +214 -0
- package/autonomy-promotion.js +280 -0
- package/build-package.mjs +25 -10
- package/capture-a11y.js +116 -0
- package/capture-local.js +85 -0
- package/capture-sync.js +99 -0
- package/consult-core.js +81 -19
- package/consult-policy.js +427 -33
- package/consult.js +25 -3
- package/delegation-sync.js +89 -0
- package/escalation-policy.sample.json +13 -0
- package/escalation.js +383 -0
- package/fingerprint-source.js +78 -4
- package/health.js +54 -0
- package/hooks/session-end.mjs +240 -0
- package/install.mjs +164 -21
- package/lib/capture-home.js +46 -0
- package/lib/domain.js +101 -0
- package/lib/is-main.js +40 -0
- package/lib/phaedo-crypto.js +17 -1
- package/package.json +26 -6
- package/pairing.js +5 -1
- package/phone-source.js +92 -12
- package/projection.js +4 -1
- package/pull.js +19 -3
- package/receipts-sync.js +197 -0
- package/receipts.js +69 -16
- package/server.js +305 -15
- package/server.json +4 -4
- package/signing-audit.js +111 -0
- package/signing-request.js +217 -0
- package/signing.js +176 -0
- package/vendor/authored-source.js +250 -0
- package/vendor/capabilities.js +203 -0
- package/vendor/capture/capture-core.js +67 -0
- package/vendor/capture/detectors.js +287 -0
- package/vendor/capture/exchanges.js +77 -0
- package/vendor/capture/model-detect.js +195 -0
- package/vendor/capture/owner.js +35 -0
- package/vendor/capture/package.json +4 -0
- package/vendor/capture/source-id.js +56 -0
- package/vendor/capture/state.js +148 -0
- package/vendor/capture/transcript.js +153 -0
- package/vendor/commitments.js +169 -0
- package/vendor/context-block.js +251 -18
- package/vendor/envelope.js +50 -4
- package/vendor/extraction/capture-combine.js +160 -0
- package/vendor/extraction/catalog.js +232 -0
- package/vendor/extraction/delta-lift.js +109 -0
- package/vendor/extraction/fingerprint-update.js +227 -0
- package/vendor/extraction/package.json +4 -0
- package/vendor/extraction/persist.js +40 -0
- package/vendor/extraction/reconcile.js +269 -0
- package/vendor/extraction/resolve.js +178 -0
- package/vendor/extraction/rule-promotion.js +137 -0
- package/vendor/kdf.js +20 -1
- package/vendor/protocol.js +7 -0
- package/vendored-files.mjs +82 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// The §5 step-3 upgrade gate — who is current, and may the A-home be retired yet.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. Step 3 deletes the fingerprint A-home, which is where authored content lives
|
|
4
|
+
// today. A surface that has not upgraded then reads NOTHING where the subject's rules used to be,
|
|
5
|
+
// and the failure is asymmetric: a missing GRANT fails safe (no `autoproceed` match, so the agent
|
|
6
|
+
// asks) but a missing PROHIBITION does not — with no `escalate`/`decline` rule present the
|
|
7
|
+
// inferred signal governs, and that can be "proceed". Retiring the A-home while any reader is
|
|
8
|
+
// behind therefore removes caution specifically, never permission.
|
|
9
|
+
//
|
|
10
|
+
// Sequencing decision 3 (config-home-v02-sequencing-2026-07-26) defines the gate: with one user
|
|
11
|
+
// it is "Randy's phone and extension are both on the new build." This module makes that a
|
|
12
|
+
// runtime fact instead of a release-day memory.
|
|
13
|
+
//
|
|
14
|
+
// THE SHAPE THAT MATTERS: this fails CLOSED, and the way it fails closed is not the obvious one.
|
|
15
|
+
// `[].every(...)` is TRUE in JavaScript, so a naive "do all peers support it?" answers YES when
|
|
16
|
+
// NO PEER HAS EVER REPORTED. That is this codebase's recurring failure exactly — silence read as
|
|
17
|
+
// success. Silence here means "you do not know", and not knowing means do not retire anything.
|
|
18
|
+
//
|
|
19
|
+
// Mirrored by mobile/src/lib/capabilities.ts; kept honest by
|
|
20
|
+
// scripts/test_mobile_capabilities_parity.mjs, which RUNS both over the same inputs.
|
|
21
|
+
|
|
22
|
+
(function () {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
/** This surface can send and receive the §5 artifact. True on both surfaces since #266/#267. */
|
|
26
|
+
const CONFIG_TRANSPORT = 'config_transport_v0.1';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The capability step 3 requires: this surface SOURCES authored content from §5 and stays
|
|
30
|
+
* correct when the A-home is absent. Deliberately distinct from CONFIG_TRANSPORT — carrying
|
|
31
|
+
* the artifact and reading from it are different claims, and conflating them is how a gate
|
|
32
|
+
* ends up opening on the wrong evidence. Nothing advertises this yet; the step-3 read path
|
|
33
|
+
* is what earns it.
|
|
34
|
+
*/
|
|
35
|
+
//
|
|
36
|
+
// ⚠️ NECESSARY BUT NOT SUFFICIENT FOR STEP 3 — found on device 2026-07-27. Every surface now
|
|
37
|
+
// genuinely READS from §5, and this gate goes green on that basis. But §5 cannot yet CARRY an
|
|
38
|
+
// authored rule between devices: mergeConfig folds only `example_corpus`, so a rule authored on
|
|
39
|
+
// the extension is discarded by the phone's merge. Correct today (standing_instructions is
|
|
40
|
+
// re-derived from the A-home, which is what actually syncs rules) and broken the moment step 3
|
|
41
|
+
// makes §5 the authored home. See scripts/test_config_sync_rules_gap.mjs — the tripwire that
|
|
42
|
+
// must be deleted, not silenced, when the merge learns to carry rules.
|
|
43
|
+
const CONFIG_HOME = 'config_home_v0.2';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What each surface implements — PER SURFACE, not one shared list.
|
|
47
|
+
*
|
|
48
|
+
* This file is loaded by the extension AND vendored into the published MCP, and the two do not
|
|
49
|
+
* become §5-native at the same moment. A single shared list would have made the agent claim
|
|
50
|
+
* CONFIG_HOME the instant the extension earned it, which is precisely the "gate opens on the
|
|
51
|
+
* wrong evidence" failure the gate exists to prevent — and it would have been invisible, since
|
|
52
|
+
* the agent would simply have started reading a home it does not understand.
|
|
53
|
+
*
|
|
54
|
+
* A surface earns CONFIG_HOME only when EVERY one of its authored-content reads resolves
|
|
55
|
+
* through §5 and it stays correct with the A-home absent.
|
|
56
|
+
*/
|
|
57
|
+
const CAPABILITIES_BY_SURFACE = {
|
|
58
|
+
// Both read sites go through §5: the injection path (context-block.js renderContextBlock)
|
|
59
|
+
// and the popup's rule display (_readStandingRules → authored-source.js).
|
|
60
|
+
extension: [CONFIG_TRANSPORT, CONFIG_HOME],
|
|
61
|
+
// The agent's authored-content readers (ap2.js, autonomy-promotion.js, consult-core.js) all
|
|
62
|
+
// take a FINGERPRINT rather than a vault, so §5 is folded in ONCE at the vd boundary
|
|
63
|
+
// (fingerprint-source.js foldConfigHome) and every one of them is §5-canonical by
|
|
64
|
+
// construction. Same boundary carries the §5.4 check.
|
|
65
|
+
agent: [CONFIG_TRANSPORT, CONFIG_HOME],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Back-compat for callers that ask without naming a surface: the conservative intersection. */
|
|
69
|
+
const CAPABILITIES = [CONFIG_TRANSPORT];
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// WHO MUST BE CURRENT depends on WHO IS ASKING. Retiring the A-home harms any surface that READS
|
|
73
|
+
// authored content, and there are three; the asker is not one of its own peers, because it knows
|
|
74
|
+
// its own capability from CAPABILITIES_BY_SURFACE — it is the code that is running.
|
|
75
|
+
//
|
|
76
|
+
// This used to be a single list, ['phone', 'agent'], which is the EXTENSION's view written as if
|
|
77
|
+
// it were universal. The moment the phone gained a gate of its own that list had it checking
|
|
78
|
+
// ITSELF and the agent while never hearing about the extension — the one peer whose readiness
|
|
79
|
+
// actually matters to it.
|
|
80
|
+
//
|
|
81
|
+
// `self` is REQUIRED in effect: an unrecognised value falls back to checking all three, which
|
|
82
|
+
// can only add blockers. The opposite default — assuming a caller meant the extension — could
|
|
83
|
+
// REMOVE one, and a gate that quietly stops checking a reader is the failure this whole module
|
|
84
|
+
// exists to prevent.
|
|
85
|
+
const ALL_READERS = ['extension', 'phone', 'agent'];
|
|
86
|
+
function readersFor(self) {
|
|
87
|
+
return ALL_READERS.includes(self) ? ALL_READERS.filter((s) => s !== self) : ALL_READERS.slice();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The extension's peer set. Kept as a named export because callers and tests reference it. */
|
|
91
|
+
const READERS = readersFor('extension');
|
|
92
|
+
|
|
93
|
+
// A report older than this is not evidence about the fleet as it stands now — a device can be
|
|
94
|
+
// reinstalled, downgraded, or simply gone. 30 days is long enough that a phone left in a drawer
|
|
95
|
+
// over a holiday does not trip the gate, short enough that a stale record cannot authorise a
|
|
96
|
+
// one-way door months later.
|
|
97
|
+
const MAX_REPORT_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
98
|
+
|
|
99
|
+
function isObj(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The payload a surface stamps into what it deposits. Deliberately a CAPABILITY LIST rather
|
|
103
|
+
* than a version number: "what can you do" survives version drift and forks, "what version are
|
|
104
|
+
* you" needs a comparison table that nobody maintains.
|
|
105
|
+
*/
|
|
106
|
+
function advertise(surface, now) {
|
|
107
|
+
if (typeof surface !== 'string' || !surface) return null;
|
|
108
|
+
// An UNKNOWN surface gets the conservative set, never the most capable one. Guessing upward
|
|
109
|
+
// here would let a surface nobody has audited close the gate's last blocker.
|
|
110
|
+
const caps = CAPABILITIES_BY_SURFACE[surface] || CAPABILITIES;
|
|
111
|
+
return {
|
|
112
|
+
surface,
|
|
113
|
+
capabilities: caps.slice(),
|
|
114
|
+
stamped_at: now || new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fold an incoming advertisement into the record of what each peer last reported. Keyed by
|
|
120
|
+
* surface, newest-wins — a surface only ever describes itself, so there is no merge conflict
|
|
121
|
+
* to resolve and no union to get wrong.
|
|
122
|
+
*/
|
|
123
|
+
function recordPeer(known, incoming) {
|
|
124
|
+
const base = isObj(known) ? known : {};
|
|
125
|
+
if (!isObj(incoming) || typeof incoming.surface !== 'string' || !incoming.surface) return base;
|
|
126
|
+
const prior = base[incoming.surface];
|
|
127
|
+
const pt = prior ? Date.parse(prior.stamped_at) || 0 : 0;
|
|
128
|
+
const it = Date.parse(incoming.stamped_at) || 0;
|
|
129
|
+
if (it <= pt) return base;
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
[incoming.surface]: {
|
|
133
|
+
capabilities: Array.isArray(incoming.capabilities) ? incoming.capabilities.slice() : [],
|
|
134
|
+
stamped_at: incoming.stamped_at,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fold a LIST of advertisements. The phone deposits its own plus any it is couriering for a
|
|
141
|
+
* paired agent, because the agent has no direct channel to the extension — the same courier
|
|
142
|
+
* role the phone already plays for capture snapshots and delegation suggestions.
|
|
143
|
+
*/
|
|
144
|
+
function recordPeers(known, list) {
|
|
145
|
+
if (!Array.isArray(list)) return isObj(known) ? known : {};
|
|
146
|
+
return list.reduce((acc, ad) => recordPeer(acc, ad), isObj(known) ? known : {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* May the A-home be retired? Returns a REASON, not just a boolean — the caller has to be able
|
|
151
|
+
* to tell the subject why their cutover is being withheld, and "false" alone cannot.
|
|
152
|
+
*
|
|
153
|
+
* Fails closed on: no record at all, a reader that has never reported, a reader that reported
|
|
154
|
+
* without the capability, and a report too old to be evidence.
|
|
155
|
+
*/
|
|
156
|
+
function retirementBlockers(known, now, opts) {
|
|
157
|
+
const at = now ? Date.parse(now) : Date.now();
|
|
158
|
+
const rec = isObj(known) ? known : {};
|
|
159
|
+
const blockers = [];
|
|
160
|
+
for (const surface of readersFor(opts && opts.self)) {
|
|
161
|
+
const entry = rec[surface];
|
|
162
|
+
if (!isObj(entry)) {
|
|
163
|
+
// The empty-record case. This is the branch `[].every(...)` would have answered "fine".
|
|
164
|
+
blockers.push({ surface, reason: 'never_reported' });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const caps = Array.isArray(entry.capabilities) ? entry.capabilities : [];
|
|
168
|
+
if (!caps.includes(CONFIG_HOME)) {
|
|
169
|
+
blockers.push({ surface, reason: 'lacks_capability' });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const stamped = Date.parse(entry.stamped_at) || 0;
|
|
173
|
+
if (!stamped || at - stamped > MAX_REPORT_AGE_MS) {
|
|
174
|
+
blockers.push({ surface, reason: 'report_too_old' });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return blockers;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Convenience over retirementBlockers. Never call this without surfacing the blockers. */
|
|
181
|
+
function canRetireAHome(known, now, opts) {
|
|
182
|
+
return retirementBlockers(known, now, opts).length === 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** One line a human can act on. */
|
|
186
|
+
function describeBlockers(blockers) {
|
|
187
|
+
if (!Array.isArray(blockers) || !blockers.length) return 'every reader is current';
|
|
188
|
+
const words = {
|
|
189
|
+
never_reported: 'has never reported in',
|
|
190
|
+
lacks_capability: 'is on an older build',
|
|
191
|
+
report_too_old: 'has not been seen recently enough to count',
|
|
192
|
+
};
|
|
193
|
+
return blockers.map((b) => `${b.surface} ${words[b.reason] || b.reason}`).join('; ');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const api = {
|
|
197
|
+
CONFIG_TRANSPORT, CONFIG_HOME, CAPABILITIES, CAPABILITIES_BY_SURFACE, READERS, ALL_READERS,
|
|
198
|
+
readersFor, MAX_REPORT_AGE_MS,
|
|
199
|
+
advertise, recordPeer, recordPeers, retirementBlockers, canRetireAHome, describeBlockers,
|
|
200
|
+
};
|
|
201
|
+
if (typeof globalThis !== 'undefined') globalThis.PhaedoCapabilities = api;
|
|
202
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
203
|
+
})();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Phaedo capture — the tier-agnostic spine: exchange_batch → signal delta.
|
|
2
|
+
//
|
|
3
|
+
// Every capture surface (the Claude Code JSONL hook, the Track B a11y helper, a future
|
|
4
|
+
// OCR tier) converges here once it has an exchange_batch (capture/transcript.js or
|
|
5
|
+
// capture/exchanges.js). This module runs the shared tail — deterministic Path-A
|
|
6
|
+
// detectors (+ optional local-model enrichment) → per-session state fold (resume-safe
|
|
7
|
+
// REPLACE) → cumulative delta lift — and returns the source='delta' signals. It does NOT
|
|
8
|
+
// touch the network: building + depositing the envelope is the transport layer's job
|
|
9
|
+
// (mcp/capture-sync.js), which keeps capture/ deposit-free and lets each surface own its
|
|
10
|
+
// pairing resolution. So a new front-end is a batch producer, not a new pipeline.
|
|
11
|
+
//
|
|
12
|
+
// Pure except the injected state file I/O (capture/state.js), exactly like the hook.
|
|
13
|
+
|
|
14
|
+
import { accumulateBatchCounts } from './detectors.js';
|
|
15
|
+
import { captureModelOpts, enrichCountsWithModel } from './model-detect.js';
|
|
16
|
+
import { loadState, saveState, applySession, cumulativeCounts, defaultStatePath } from './state.js';
|
|
17
|
+
import { liftDeltaSignals } from '../extraction/delta-lift.js';
|
|
18
|
+
|
|
19
|
+
// Fold one batch into this surface's cumulative signal delta.
|
|
20
|
+
// batch — an exchange_batch ({ exchanges, user_prompts, session_local_id }).
|
|
21
|
+
// opts:
|
|
22
|
+
// sourceId — the per-machine+surface source_device_id (capture/source-id.js).
|
|
23
|
+
// surface — surface tag echoed into the result (e.g. 'a11y-macos').
|
|
24
|
+
// statePath — durable per-surface state file (default: state.js defaultStatePath).
|
|
25
|
+
// sessionLocalId — STABLE per-conversation id → per-session REPLACE (idempotent re-reads).
|
|
26
|
+
// now — ISO timestamp (fixed for deterministic tests).
|
|
27
|
+
// captureModel — option (b) model opts, or undefined to auto-resolve from env; falsy = off.
|
|
28
|
+
// env — process.env (for captureModelOpts auto-resolution).
|
|
29
|
+
// Returns { signals, counts, modelAdded, sourceId, surface } — never throws on a bad
|
|
30
|
+
// batch (empty → zero signals); state I/O failures are swallowed (counters just restart).
|
|
31
|
+
export async function foldBatchToSignals(batch, opts = {}) {
|
|
32
|
+
const statePath = opts.statePath || defaultStatePath();
|
|
33
|
+
const nowIso = opts.now || new Date().toISOString();
|
|
34
|
+
const sourceId = opts.sourceId || null;
|
|
35
|
+
const sessionLocalId = opts.sessionLocalId || (batch && batch.session_local_id) || null;
|
|
36
|
+
|
|
37
|
+
const exchanges = (batch && batch.exchanges) || [];
|
|
38
|
+
const prompts = (batch && Array.isArray(batch.user_prompts)) ? batch.user_prompts : exchanges;
|
|
39
|
+
if (!prompts.length) return { signals: [], counts: null, modelAdded: 0, sourceId, surface: opts.surface || null };
|
|
40
|
+
|
|
41
|
+
let counts = accumulateBatchCounts(batch);
|
|
42
|
+
|
|
43
|
+
// Option (b): fold local-model classifications of the regex-MISS user messages into
|
|
44
|
+
// this session's counts BEFORE the state apply, so re-reads stay resume-safe and the
|
|
45
|
+
// deposit stays pure delta. Opt-in + fail-safe (any error leaves regex counts intact).
|
|
46
|
+
let modelAdded = 0;
|
|
47
|
+
const modelOpts = opts.captureModel !== undefined ? opts.captureModel : captureModelOpts(opts.env);
|
|
48
|
+
if (modelOpts && modelOpts.model) {
|
|
49
|
+
try {
|
|
50
|
+
const enriched = await enrichCountsWithModel(batch, counts, modelOpts);
|
|
51
|
+
counts = enriched.counts;
|
|
52
|
+
modelAdded = enriched.added;
|
|
53
|
+
} catch { /* model down/slow/garbage → regex-only, unchanged */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let state;
|
|
57
|
+
try {
|
|
58
|
+
state = applySession(loadState(statePath), sourceId, sessionLocalId, counts, { now: nowIso });
|
|
59
|
+
try { saveState(statePath, state); } catch { /* state loss just restarts counters — never fatal */ }
|
|
60
|
+
} catch {
|
|
61
|
+
// A corrupt state file must never break capture: fold against an empty baseline.
|
|
62
|
+
state = applySession(null, sourceId, sessionLocalId, counts, { now: nowIso });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const signals = liftDeltaSignals(cumulativeCounts(state, sourceId).behavioral_signals, nowIso);
|
|
66
|
+
return { signals, counts, modelAdded, sourceId, surface: opts.surface || null };
|
|
67
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Phaedo capture — Path A detectors (shared mirror of the extension's tables).
|
|
2
|
+
//
|
|
3
|
+
// Track A option (a): the Claude Code / Cowork capture hook produces REAL signal by
|
|
4
|
+
// running the SAME deterministic Path-A detectors the Chrome extension runs in
|
|
5
|
+
// content.js — the correction patterns (whose counters delta-lift turns into §5.1
|
|
6
|
+
// signals), the positive-reinforcement patterns, and the mental-model markers.
|
|
7
|
+
//
|
|
8
|
+
// MIRROR, not import: content.js is a plain (non-module) content script, so it
|
|
9
|
+
// cannot be imported here and this module cannot be loaded there without an MV3
|
|
10
|
+
// module-loading change. Following the repo's vendored-copy precedent
|
|
11
|
+
// (context-block.js), the three tables below are a byte-exact mirror of
|
|
12
|
+
// content.js, and drift is impossible in practice because
|
|
13
|
+
// scripts/test_capture_detectors.mjs extracts the tables from content.js source
|
|
14
|
+
// and compares every pattern (source + flags + layer/signal/framework) 1:1 —
|
|
15
|
+
// any edit to either side without the other fails CI.
|
|
16
|
+
//
|
|
17
|
+
// The detect functions are PURE ports (no globals, no accumulation): the
|
|
18
|
+
// extension's detectCorrection mutates memSignals; here the caller accumulates.
|
|
19
|
+
// Matching semantics are identical: corrections are FIRST-match-only per message,
|
|
20
|
+
// mental models at most once per framework per message.
|
|
21
|
+
|
|
22
|
+
// ─── Correction patterns (all 8 layers) — mirror of content.js ───────────────
|
|
23
|
+
|
|
24
|
+
export const CORRECTION_PATTERNS = [
|
|
25
|
+
// Surface — depth
|
|
26
|
+
{ pattern: /\b(too long|too verbose|be (more )?concise|shorter|brief(er)?|summarize that|less words|too wordy|cut it down)\b/i, layer: 'surface', signal: 'too_long' },
|
|
27
|
+
{ pattern: /\b(too short|more detail|elaborate|expand on|be more thorough|not enough|flesh it out|missing context)\b/i, layer: 'surface', signal: 'too_short' },
|
|
28
|
+
// Surface — formatting
|
|
29
|
+
{ pattern: /\bdon'?t use bullets?\b|\bno bullets?\b|\buse (prose|paragraphs?)\b|\bno lists?\b/i, layer: 'surface', signal: 'avoid_bullets' },
|
|
30
|
+
{ pattern: /\buse (bullet|numbered) (points?|lists?)\b|\blist (it|them|that) out\b|\bbullet (format|points?)\b/i, layer: 'surface', signal: 'prefer_bullets' },
|
|
31
|
+
{ pattern: /\b(no headers?|no sections?|no headings?|don'?t section|remove (the )?headers?)\b/i, layer: 'surface', signal: 'no_headers' },
|
|
32
|
+
{ pattern: /\b(add headers?|use headers?|section it|organize with|with headings?)\b/i, layer: 'surface', signal: 'use_headers' },
|
|
33
|
+
{ pattern: /\b(table format|in a table|tabular|use a table|as a table)\b/i, layer: 'surface', signal: 'use_table' },
|
|
34
|
+
{ pattern: /\b(code block|in code|formatted code|wrap (that|it) in code)\b/i, layer: 'surface', signal: 'use_code_block' },
|
|
35
|
+
// Surface — tone
|
|
36
|
+
{ pattern: /\b(too formal|less formal|more casual|relax the tone|conversational(ly)?|informal)\b/i, layer: 'surface', signal: 'prefers_casual' },
|
|
37
|
+
{ pattern: /\b(too casual|more formal|professional(ly)?|keep it formal)\b/i, layer: 'surface', signal: 'prefers_formal' },
|
|
38
|
+
{ pattern: /\b(too roundabout|get to the point|be direct|skip the preamble|lead with|bottom.?line (up )?front)\b/i, layer: 'surface', signal: 'more_direct' },
|
|
39
|
+
{ pattern: /\b(no (intro|preamble|opener)|skip the intro|don'?t start with|cut the opener|dive right in)\b/i, layer: 'surface', signal: 'no_preamble' },
|
|
40
|
+
{ pattern: /\b(no examples?|skip the examples?|I don'?t need examples?|without examples?)\b/i, layer: 'surface', signal: 'no_examples' },
|
|
41
|
+
{ pattern: /\b(give me an example|show me an example|illustrate|for example|can you show|concrete example)\b/i, layer: 'surface', signal: 'wants_examples' },
|
|
42
|
+
{ pattern: /\b(simpler words?|plain(er)? (language|english)|avoid jargon|less jargon|plain terms?)\b/i, layer: 'surface', signal: 'plainer_language' },
|
|
43
|
+
|
|
44
|
+
// Behavioral — autonomy and initiative
|
|
45
|
+
{ pattern: /\b(just do it|stop asking|don'?t (ask|check)|fewer questions?|just (go|start|write|build|do)|no need to ask)\b/i, layer: 'behavioral', signal: 'fewer_questions' },
|
|
46
|
+
{ pattern: /\b(ask (me )?first|confirm before|check (with me|before)|get my (input|approval)|don'?t assume)\b/i, layer: 'behavioral', signal: 'wants_confirmation' },
|
|
47
|
+
{ pattern: /\b(show your (work|reasoning)|explain your reasoning|walk me through|how (did you|do you) (get|arrive))\b/i, layer: 'behavioral', signal: 'show_reasoning' },
|
|
48
|
+
{ pattern: /\b(just (give me|the) (answer|result)|skip the explanation|don'?t explain (how|why)|bottom line only)\b/i, layer: 'behavioral', signal: 'skip_reasoning' },
|
|
49
|
+
{ pattern: /\b(just pick (one|it)|make (a|the) decision|too many options?|don'?t give me options?|choose for me)\b/i, layer: 'behavioral', signal: 'fewer_options' },
|
|
50
|
+
{ pattern: /\b(give me (options?|alternatives?|choices?)|what are my (options?|choices?)|several approaches?|show alternatives?)\b/i, layer: 'behavioral', signal: 'wants_options' },
|
|
51
|
+
{ pattern: /\b(what else (should I|am I missing)|be proactive|anticipate|what haven'?t I|what am I missing)\b/i, layer: 'behavioral', signal: 'wants_proactive' },
|
|
52
|
+
{ pattern: /\b(just (what I asked|the question)|don'?t add (extra|more|additional)|stick to (the question|what I said)|only what I asked)\b/i, layer: 'behavioral', signal: 'stay_scoped' },
|
|
53
|
+
{ pattern: /\b(let'?s (refine|iterate|keep going)|more (passes?|iterations?)|continue (from|where)|iterate on this)\b/i, layer: 'behavioral', signal: 'iterate_more' },
|
|
54
|
+
|
|
55
|
+
// Domain — expertise calibration
|
|
56
|
+
{ pattern: /\b(I (already )?know (that|this)|skip the basics?|assume I know|don'?t explain (that|this)|over.?explaining)\b/i, layer: 'domain_and_expertise', signal: 'overexplaining' },
|
|
57
|
+
{ pattern: /\b(too technical|simpler( terms?| language)?|(plain|plain.?english)|explain like (I'?m|a)|non.?technical)\b/i, layer: 'domain_and_expertise', signal: 'too_technical' },
|
|
58
|
+
{ pattern: /\b(go deeper|more technical|implementation detail|under the hood|technical depth?|deeper dive|specifics?)\b/i, layer: 'domain_and_expertise', signal: 'wants_depth' },
|
|
59
|
+
{ pattern: /\b(cite (your )?sources?|where (does this|did you)|source for that|reference for this|where did you get)\b/i, layer: 'domain_and_expertise', signal: 'wants_sources' },
|
|
60
|
+
{ pattern: /\b(just the concept|high.?level( only)?|conceptual (understanding|overview)|the idea not the code)\b/i, layer: 'domain_and_expertise', signal: 'conceptual_only' },
|
|
61
|
+
{ pattern: /\b(show (me )?(the )?code|implementation|how (to implement|do I build)|concrete (example|code|steps?))\b/i, layer: 'domain_and_expertise', signal: 'wants_implementation' },
|
|
62
|
+
{ pattern: /\b(too broad|narrow it( down)?|focus on .{1,40}? specifically|more focused|scope (it )?down)\b/i, layer: 'domain_and_expertise', signal: 'too_broad' },
|
|
63
|
+
|
|
64
|
+
// Temporal — urgency and project phase
|
|
65
|
+
{ pattern: /\b(quick (version|answer|summary)|TL;?DR|key points? only|brief (version|summary)|in a hurry|short on time|fast (answer|version))\b/i, layer: 'temporal_and_context', signal: 'urgent' },
|
|
66
|
+
{ pattern: /\b(thorough(ly)?( is fine)?|take (the )?space|I have time|no hurry|comprehensive (is fine|please))\b/i, layer: 'temporal_and_context', signal: 'no_rush' },
|
|
67
|
+
{ pattern: /\b(just exploring|early (stage|days)|high.?level for now|don'?t go too deep (yet)?|preliminary)\b/i, layer: 'temporal_and_context', signal: 'exploring_phase' },
|
|
68
|
+
{ pattern: /\b(ready to build|implementation mode|let'?s get specific|concrete now|production.?ready|ship it)\b/i, layer: 'temporal_and_context', signal: 'build_mode' },
|
|
69
|
+
{ pattern: /\b(big (picture|view)|strategy (level|only)|not the details|30[,.]?000.?foot|overview first|macro)\b/i, layer: 'temporal_and_context', signal: 'big_picture' },
|
|
70
|
+
{ pattern: /\b(get (granular|specific|detailed)|deep in the details|implementation specifics?|micro.?level)\b/i, layer: 'temporal_and_context', signal: 'detail_mode' },
|
|
71
|
+
|
|
72
|
+
// Collaboration — audience and voice
|
|
73
|
+
{ pattern: /\b(write for (a )?non.?technical|simplify for (my )?audience|they don'?t know|lay(man'?s?) terms?|accessible)\b/i, layer: 'collaboration_and_relationship', signal: 'non_technical_audience' },
|
|
74
|
+
{ pattern: /\b(this is for (engineers?|developers?|technical)|technical audience|assume (they know|technical knowledge))\b/i, layer: 'collaboration_and_relationship', signal: 'technical_audience' },
|
|
75
|
+
{ pattern: /\b(write in my (voice|style)|sound like me|match my (voice|tone|style)|in my (words|style))\b/i, layer: 'collaboration_and_relationship', signal: 'match_voice' },
|
|
76
|
+
{ pattern: /\b(for a client|external (audience|use|doc)|this is (public|external|client.?facing)|publishing this)\b/i, layer: 'collaboration_and_relationship', signal: 'external_audience' },
|
|
77
|
+
{ pattern: /\b(too jargon.?y for them|they won'?t understand|simpler for (this|my) audience|dumb (it )?down for)\b/i, layer: 'collaboration_and_relationship', signal: 'too_jargony_for_audience' },
|
|
78
|
+
|
|
79
|
+
// Decision — risk and certainty
|
|
80
|
+
{ pattern: /\b(just (decide|pick|choose)|make (a|the) call|stop hedging|give me a recommendation|be decisive|commit to)\b/i, layer: 'decision_and_risk', signal: 'wants_decisiveness' },
|
|
81
|
+
{ pattern: /\b(show (me )?(my )?options?|I want to choose|don'?t decide for me|lay out (the )?choices?|what are (my )?options?)\b/i, layer: 'decision_and_risk', signal: 'wants_options_decision' },
|
|
82
|
+
{ pattern: /\b(what are the (trade.?offs?|risks?)|pros and cons|caveats?|what could go wrong|downsides?|what'?s the catch)\b/i, layer: 'decision_and_risk', signal: 'wants_nuance' },
|
|
83
|
+
{ pattern: /\b(be (bold|more decisive|stronger)|strong(er)? recommendation|which is better|don'?t hedge|be definitive)\b/i, layer: 'decision_and_risk', signal: 'wants_bold_call' },
|
|
84
|
+
{ pattern: /\b(conservative (approach|option|choice)|safer (option|path|choice)|low.?risk|cautious)\b/i, layer: 'decision_and_risk', signal: 'wants_conservative' },
|
|
85
|
+
{ pattern: /\b(I need certainty|nail it down|definitive (answer|yes|no)|how confident|is this certain)\b/i, layer: 'decision_and_risk', signal: 'needs_certainty' },
|
|
86
|
+
{ pattern: /\b(ballpark (is fine|estimate|answer)|rough estimate|in the ballpark|order of magnitude|approximate(ly)?( is fine)?)\b/i, layer: 'decision_and_risk', signal: 'rough_estimate_ok' },
|
|
87
|
+
|
|
88
|
+
// Creative — style and originality
|
|
89
|
+
{ pattern: /\b(too conventional|be (more )?creative|unexpected (approach|angle|idea)|think (differently|outside)|original)\b/i, layer: 'creative', signal: 'wants_creative' },
|
|
90
|
+
{ pattern: /\b(simpler|more elegant|too complex|less is more|strip (it )?down|over.?engineered|over.?complicated)\b/i, layer: 'creative', signal: 'wants_elegance' },
|
|
91
|
+
{ pattern: /\b(standard (approach|solution)|conventional|don'?t overthink|keep it (simple|straightforward|standard))\b/i, layer: 'creative', signal: 'wants_conventional' },
|
|
92
|
+
{ pattern: /\b(more original|surprise me|not the obvious|unconventional|different (angle|approach|way)|unexpected)\b/i, layer: 'creative', signal: 'wants_originality' },
|
|
93
|
+
|
|
94
|
+
// Meta — transparency and self-awareness
|
|
95
|
+
{ pattern: /\b(don'?t explain what you'?re doing|just do it|skip the (meta|commentary|preamble)|no meta)\b/i, layer: 'big_picture', signal: 'skip_meta' },
|
|
96
|
+
{ pattern: /\b(show your reasoning|think out loud|walk me through your (logic|thinking|approach)|what'?s your (plan|approach|thinking))\b/i, layer: 'big_picture', signal: 'show_meta' },
|
|
97
|
+
{ pattern: /\b(what (assumptions?|are you assuming)|be transparent|tell me your reasoning|what'?s your (basis|rationale))\b/i, layer: 'big_picture', signal: 'wants_transparency' },
|
|
98
|
+
{ pattern: /\b(commit|stop second.?guessing|be confident|don'?t hedge|pick and go|just (pick|commit|decide))\b/i, layer: 'big_picture', signal: 'less_second_guessing' },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// Positive reinforcement — mirror of content.js
|
|
102
|
+
export const POSITIVE_PATTERNS = [
|
|
103
|
+
/\b(perfect(ly)?( (done|answer|response|format|length|tone))?)\b/i,
|
|
104
|
+
/\b(exactly (right|what I (wanted|needed|meant|was looking for)))\b/i,
|
|
105
|
+
/\byes[,!]?\s+(that'?s?|exactly|perfect|right|nailed|spot on)\b/i,
|
|
106
|
+
/\b(love (this|it|that|the (way|approach|style|format|tone)))\b/i,
|
|
107
|
+
/\b(great (call|choice|approach|idea|response|answer|format|depth))\b/i,
|
|
108
|
+
/\bnailed (it|the (tone|format|depth|length))\b/i,
|
|
109
|
+
/\b(keep (doing|this|that) (up|like this|way))\b/i,
|
|
110
|
+
/\bspot on\b/i,
|
|
111
|
+
/\b(this is (great|perfect|exactly (right|what I needed)))\b/i,
|
|
112
|
+
/\b(much better( now)?)\b/i,
|
|
113
|
+
/\b(that (works|worked) (perfectly|great|well))\b/i,
|
|
114
|
+
/\b(good (call|instinct|judgment|read|approach))\b/i,
|
|
115
|
+
/\bright (level|tone|depth|format|approach|amount|length)\b/i,
|
|
116
|
+
/\b(sounds like me|matched my (voice|style|tone))\b/i,
|
|
117
|
+
/\b(exactly the (right|level|depth|amount|tone) I (needed|wanted))\b/i,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Mental-model markers — mirror of content.js
|
|
121
|
+
export const MENTAL_MODEL_PATTERNS = [
|
|
122
|
+
// First-principles reasoning — reasons up from fundamentals
|
|
123
|
+
{ pattern: /\bfirst principles?\b/i, framework: 'first_principles' },
|
|
124
|
+
{ pattern: /\b(root cause|underlying (reason|cause|mechanism|principle))\b/i, framework: 'first_principles' },
|
|
125
|
+
{ pattern: /\bfrom (the ground up|scratch)\b/i, framework: 'first_principles' },
|
|
126
|
+
{ pattern: /\b(core|base|underlying) assumptions?\b/i, framework: 'first_principles' },
|
|
127
|
+
{ pattern: /\bwhy does (that|this|it) (actually |even |really )?work\b/i, framework: 'first_principles' },
|
|
128
|
+
{ pattern: /\bbreak (it|this|that) down to (the )?(basics|fundamentals)\b/i, framework: 'first_principles' },
|
|
129
|
+
{ pattern: /\bfundamental(ly)? (question|issue|problem|reason|why)\b/i, framework: 'first_principles' },
|
|
130
|
+
{ pattern: /\bwhat'?s really (going on|happening|behind)\b/i, framework: 'first_principles' },
|
|
131
|
+
|
|
132
|
+
// Systems thinking — feedback loops, interconnection, the whole system
|
|
133
|
+
{ pattern: /\bfeedback loops?\b/i, framework: 'systems_thinking' },
|
|
134
|
+
{ pattern: /\bthe (whole|entire|broader|bigger) (system|picture)\b|\bsystem.?(wide|level)\b/i, framework: 'systems_thinking' },
|
|
135
|
+
{ pattern: /\binterdependen|interconnect|interrelat/i, framework: 'systems_thinking' },
|
|
136
|
+
{ pattern: /\bemergent\b/i, framework: 'systems_thinking' },
|
|
137
|
+
{ pattern: /\bholistic(ally)?\b/i, framework: 'systems_thinking' },
|
|
138
|
+
{ pattern: /\bmoving parts\b/i, framework: 'systems_thinking' },
|
|
139
|
+
{ pattern: /\b(vicious|virtuous) (cycle|circle)\b/i, framework: 'systems_thinking' },
|
|
140
|
+
{ pattern: /\bcause and effect\b/i, framework: 'systems_thinking' },
|
|
141
|
+
{ pattern: /\bsystem(ic|s) (dynamics?|behaviou?r)\b/i, framework: 'systems_thinking' },
|
|
142
|
+
|
|
143
|
+
// Theory of constraints — bottleneck / limiting-factor framing
|
|
144
|
+
{ pattern: /\bbottleneck/i, framework: 'theory_of_constraints' },
|
|
145
|
+
{ pattern: /\b(the|a|key|main|real) constraint\b/i, framework: 'theory_of_constraints' },
|
|
146
|
+
{ pattern: /\blimiting factor\b/i, framework: 'theory_of_constraints' },
|
|
147
|
+
{ pattern: /\bwhat'?s (blocking|slowing|the blocker)\b|\bholding .{1,20} back\b/i, framework: 'theory_of_constraints' },
|
|
148
|
+
{ pattern: /\bcritical path\b/i, framework: 'theory_of_constraints' },
|
|
149
|
+
{ pattern: /\brate.?limiting\b/i, framework: 'theory_of_constraints' },
|
|
150
|
+
{ pattern: /\bchoke ?point\b/i, framework: 'theory_of_constraints' },
|
|
151
|
+
{ pattern: /\bgating (factor|item|step|issue)\b/i, framework: 'theory_of_constraints' },
|
|
152
|
+
{ pattern: /\bweakest link\b/i, framework: 'theory_of_constraints' },
|
|
153
|
+
|
|
154
|
+
// Second-order effects — consequences of consequences
|
|
155
|
+
{ pattern: /\bsecond.?order\b/i, framework: 'second_order_effects' },
|
|
156
|
+
{ pattern: /\bdownstream (effects?|consequences?|impacts?)\b/i, framework: 'second_order_effects' },
|
|
157
|
+
{ pattern: /\bknock.?on (effects?|consequences?|impacts?)\b/i, framework: 'second_order_effects' },
|
|
158
|
+
{ pattern: /\bripple (effects?|through|across)\b/i, framework: 'second_order_effects' },
|
|
159
|
+
{ pattern: /\bunintended consequences?\b/i, framework: 'second_order_effects' },
|
|
160
|
+
{ pattern: /\b(and )?then what happens\b/i, framework: 'second_order_effects' },
|
|
161
|
+
{ pattern: /\bcascad(e|es)\b/i, framework: 'second_order_effects' },
|
|
162
|
+
{ pattern: /\bdown the (line|road)\b/i, framework: 'second_order_effects' },
|
|
163
|
+
{ pattern: /\b(long|longer).?term (effects?|consequences?|impacts?)\b/i, framework: 'second_order_effects' },
|
|
164
|
+
{ pattern: /\bwhat (does|will) (that|this|it) lead to\b/i, framework: 'second_order_effects' },
|
|
165
|
+
|
|
166
|
+
// Trade-off reasoning — explicit cost/benefit weighing
|
|
167
|
+
{ pattern: /\btrade.?offs?\b/i, framework: 'tradeoff_reasoning' },
|
|
168
|
+
{ pattern: /\bat the (cost|expense) of\b/i, framework: 'tradeoff_reasoning' },
|
|
169
|
+
{ pattern: /\bin exchange for\b/i, framework: 'tradeoff_reasoning' },
|
|
170
|
+
{ pattern: /\bpros and cons\b/i, framework: 'tradeoff_reasoning' },
|
|
171
|
+
{ pattern: /\bwhat (do|would) (we|i) (lose|give up|sacrifice)\b/i, framework: 'tradeoff_reasoning' },
|
|
172
|
+
{ pattern: /\bcost.?benefit\b|\bnet (gain|benefit|positive|negative)\b/i, framework: 'tradeoff_reasoning' },
|
|
173
|
+
{ pattern: /\bupsides? and downsides?\b/i, framework: 'tradeoff_reasoning' },
|
|
174
|
+
{ pattern: /\bworth the (cost|trade.?off|effort|risk)\b/i, framework: 'tradeoff_reasoning' },
|
|
175
|
+
{ pattern: /\bzero.?sum\b/i, framework: 'tradeoff_reasoning' },
|
|
176
|
+
|
|
177
|
+
// Analogical reasoning — reasoning from similar cases
|
|
178
|
+
{ pattern: /\b(similar to|analogous to)\b/i, framework: 'analogical_reasoning' },
|
|
179
|
+
{ pattern: /\breminds me of\b/i, framework: 'analogical_reasoning' },
|
|
180
|
+
{ pattern: /\bsame (idea|pattern|principle|concept) as\b/i, framework: 'analogical_reasoning' },
|
|
181
|
+
{ pattern: /\b(by analogy|analog(y|ous))\b/i, framework: 'analogical_reasoning' },
|
|
182
|
+
{ pattern: /\bthe .{1,30} equivalent of\b/i, framework: 'analogical_reasoning' },
|
|
183
|
+
{ pattern: /\bthink of (it|this) (like|as)\b/i, framework: 'analogical_reasoning' },
|
|
184
|
+
{ pattern: /\bkind of like\b/i, framework: 'analogical_reasoning' },
|
|
185
|
+
{ pattern: /\bparallels? (to|with|between)\b/i, framework: 'analogical_reasoning' },
|
|
186
|
+
|
|
187
|
+
// Decomposition — break a problem into parts
|
|
188
|
+
{ pattern: /\bbreak (it|this|that|the problem|things?) (down|up|apart|into)\b/i, framework: 'decomposition' },
|
|
189
|
+
{ pattern: /\bstep.?by.?step\b/i, framework: 'decomposition' },
|
|
190
|
+
{ pattern: /\bsmaller (parts|pieces|chunks|problems?|steps?|tasks?)\b/i, framework: 'decomposition' },
|
|
191
|
+
{ pattern: /\bdecompos/i, framework: 'decomposition' },
|
|
192
|
+
{ pattern: /\bsub.?(problems?|tasks?|components?)\b/i, framework: 'decomposition' },
|
|
193
|
+
{ pattern: /\b(one|each) (piece|part|step|chunk|thing) at a time\b/i, framework: 'decomposition' },
|
|
194
|
+
{ pattern: /\b(piece by piece|bit by bit)\b/i, framework: 'decomposition' },
|
|
195
|
+
{ pattern: /\bdivide and conquer\b/i, framework: 'decomposition' },
|
|
196
|
+
{ pattern: /\bchunk (it|this|that) (up|out|down)\b/i, framework: 'decomposition' },
|
|
197
|
+
|
|
198
|
+
// Scenario planning — contingencies, worst/best case
|
|
199
|
+
{ pattern: /\bworst.?case\b/i, framework: 'scenario_planning' },
|
|
200
|
+
{ pattern: /\bbest.?case\b/i, framework: 'scenario_planning' },
|
|
201
|
+
{ pattern: /\bcontingenc/i, framework: 'scenario_planning' },
|
|
202
|
+
{ pattern: /\b(edge|corner) cases?\b/i, framework: 'scenario_planning' },
|
|
203
|
+
{ pattern: /\bplan for (the )?(failure|downside|worst)\b/i, framework: 'scenario_planning' },
|
|
204
|
+
{ pattern: /\bif .{1,30} (fails?|breaks?|goes wrong)\b/i, framework: 'scenario_planning' },
|
|
205
|
+
{ pattern: /\bwhat'?s the worst that\b/i, framework: 'scenario_planning' },
|
|
206
|
+
{ pattern: /\b(fall.?back plan|plan b)\b/i, framework: 'scenario_planning' },
|
|
207
|
+
{ pattern: /\bdownside (risk|scenario|case)\b/i, framework: 'scenario_planning' },
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
// ─── Pure detect functions (extension semantics, caller accumulates) ─────────
|
|
211
|
+
|
|
212
|
+
// A correction/positive/framework cue is a short phrase; nobody types a 200 KB
|
|
213
|
+
// correction. Detect over only the first slice of a message — this both bounds
|
|
214
|
+
// worst-case regex cost (defense-in-depth against a pathological paste on top of
|
|
215
|
+
// the bounded patterns) and keeps semantics identical for real prompts. 8 KB is
|
|
216
|
+
// far longer than any genuine steering message.
|
|
217
|
+
const MAX_DETECT_CHARS = 8192;
|
|
218
|
+
const clip = (text) => { const s = String(text || ''); return s.length > MAX_DETECT_CHARS ? s.slice(0, MAX_DETECT_CHARS) : s; };
|
|
219
|
+
|
|
220
|
+
// First-match-only per message, same as content.js detectCorrection.
|
|
221
|
+
export function detectCorrection(text) {
|
|
222
|
+
const s = clip(text);
|
|
223
|
+
for (const { pattern, layer, signal } of CORRECTION_PATTERNS) {
|
|
224
|
+
if (pattern.test(s)) return { layer, signal };
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function detectPositive(text) {
|
|
230
|
+
const s = clip(text);
|
|
231
|
+
return POSITIVE_PATTERNS.some((pattern) => pattern.test(s));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// At most once per framework per message, same as content.js detectMentalModels.
|
|
235
|
+
export function detectMentalModels(text) {
|
|
236
|
+
const s = clip(text);
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
for (const { pattern, framework } of MENTAL_MODEL_PATTERNS) {
|
|
239
|
+
if (seen.has(framework)) continue;
|
|
240
|
+
if (pattern.test(s)) seen.add(framework);
|
|
241
|
+
}
|
|
242
|
+
return [...seen];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Run all three detectors over a batch's user messages (transcript.js
|
|
246
|
+
// recordsToBatch output) and return the SESSION's counts in the shapes the rest
|
|
247
|
+
// of the pipeline expects. Also fills each exchange's delta_annotations in place
|
|
248
|
+
// (the Loop-2 per-exchange annotation shape), so a future model backend (option
|
|
249
|
+
// (b)) consumes the identical batch the web path produces.
|
|
250
|
+
// → { behavioral_signals: { layer: { signal: n } }, // phaedo_behavioral_signals shape
|
|
251
|
+
// positive_count, mental_models: { framework: n }, message_count }
|
|
252
|
+
export function accumulateBatchCounts(batch) {
|
|
253
|
+
const exchanges = (batch && batch.exchanges) || [];
|
|
254
|
+
// Path-A counts come from EVERY sent user prompt (the web path counts at send time),
|
|
255
|
+
// so an interrupted / unanswered correction still counts. Fall back to the exchanges'
|
|
256
|
+
// user_msg when user_prompts is absent (hand-built batches in older tests).
|
|
257
|
+
const prompts = (batch && Array.isArray(batch.user_prompts))
|
|
258
|
+
? batch.user_prompts
|
|
259
|
+
: exchanges.map((e) => e.user_msg);
|
|
260
|
+
const behavioral = {};
|
|
261
|
+
const mental = {};
|
|
262
|
+
let positive = 0;
|
|
263
|
+
for (const text of prompts) {
|
|
264
|
+
const corr = detectCorrection(text);
|
|
265
|
+
if (corr) {
|
|
266
|
+
if (!behavioral[corr.layer]) behavioral[corr.layer] = {};
|
|
267
|
+
behavioral[corr.layer][corr.signal] = (behavioral[corr.layer][corr.signal] || 0) + 1;
|
|
268
|
+
}
|
|
269
|
+
if (detectPositive(text)) positive++;
|
|
270
|
+
for (const fw of detectMentalModels(text)) mental[fw] = (mental[fw] || 0) + 1;
|
|
271
|
+
}
|
|
272
|
+
// Per-exchange annotations (for the future model/Loop-2 backend) stay keyed to complete exchanges.
|
|
273
|
+
for (const ex of exchanges) {
|
|
274
|
+
const corr = detectCorrection(ex.user_msg);
|
|
275
|
+
ex.delta_annotations = {
|
|
276
|
+
corrections: corr ? [corr] : [],
|
|
277
|
+
positive: detectPositive(ex.user_msg),
|
|
278
|
+
mental_models: detectMentalModels(ex.user_msg),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
behavioral_signals: behavioral,
|
|
283
|
+
positive_count: positive,
|
|
284
|
+
mental_models: mental,
|
|
285
|
+
message_count: prompts.length,
|
|
286
|
+
};
|
|
287
|
+
}
|