- label: INITIALIZE RUN
type: group
steps:
- label: RESET FRAME SERIES
type: calc
func: set
param: shots
value: ''
format: ''
- label: RESET CAPTURED COUNTER
type: calc
func: set
param: capturedFrames
value: '0'
format: ''
- label: RESET FRAME COUNT
type: calc
func: set
param: shotCount
value: '0'
format: ''
- label: RESET VISION RECORD
type: calc
func: set
param: notes
value: ''
format: ''
- label: RESET SEGMENT BUFFER
type: calc
func: set
param: segmentNotes
value: ''
format: ''
- label: RESET BATCH BUFFER
type: calc
func: set
param: batchImages
value: ''
format: ''
- label: RESET SEGMENT COUNTER
type: calc
func: set
param: segment
value: '0'
format: ''
- label: RESET FAILED SEGMENTS
type: calc
func: set
param: failedSegments
value: '0'
format: ''
- label: RESET ANSWER
type: calc
func: set
param: gpt
value: ''
format: ''
- label: RESET FOLLOW UP STATE
type: calc
func: set
param: change
value: ''
format: ''
- label: RESET REPAIR BUFFER
type: calc
func: set
param: repairImages
value: ''
format: ''
- label: RESET REPAIR MODE
type: calc
func: set
param: repairMode
value: 'no'
format: ''
- label: RESET CHROME VERDICT
type: calc
func: set
param: chromeVerdict
value: ''
format: ''
- label: RESET RAW READING
type: calc
func: set
param: rawSegment
value: ''
format: ''
- label: RESET SCOPE NOTE
type: calc
func: set
param: scopeNote
value: ''
format: ''
- label: RESET PROGRESS LINE
type: calc
func: set
param: progress
value: ''
format: ''
- label: RESET SCRATCH SLOT
type: calc
func: set
param: scratch
value: ''
format: ''
- label: RESET REPAIR COUNT
type: calc
func: set
param: repairCount
value: ''
format: ''
- label: RESET PROMPT PAYLOAD
type: calc
func: set
param: payload
value: ''
format: ''
- label: RESET PDF PAGE READ
type: calc
func: set
param: pdfPageRead
value: ''
format: ''
- label: RESET PDF PROBE
type: calc
func: set
param: pdfProbe
value: ''
format: ''
- label: RESET READING FOCUS
type: calc
func: set
param: focusChoice
value: ''
format: ''
- label: RESET FOCUS BRIEF
type: calc
func: set
param: focus
value: ''
format: ''
- label: RESET OUTPUT FORMAT
type: calc
func: set
param: format
value: ''
format: ''
- label: RESET HANDOVER SLOT
type: calc
func: set
param: handoff
value: ''
format: ''
- label: RESET CAPTURE STATE
type: calc
func: set
param: capture
value: ''
format: ''
- label: RESET THE PASSAGE INDEX
type: calc
func: set
param: passages
value: ''
format: ''
- label: RESET THE EVIDENCE SLOT
type: calc
func: set
param: evidence
value: ''
format: ''
- label: RESET THE SELECTION
type: calc
func: set
param: selection
value: ''
format: ''
- label: RESET THE RETRIEVAL QUERY
type: calc
func: set
param: query
value: ''
format: ''
- label: RESET THE CITATION CHECK
type: calc
func: set
param: citationCheck
value: ''
format: ''
- label: RESET THE CITATION RULE
type: calc
func: set
param: citationRule
value: ''
format: ''
- label: RESET THE PENDING ANSWER
type: calc
func: set
param: pendingAnswer
value: ''
format: ''
- label: DEFAULT TO THE WHOLE RECORD
type: calc
func: set
param: method
value: full
format: ''
- label: RESET THE RECORD SIZE
type: calc
func: set
param: recordChars
value: ''
format: ''
- label: RESET THE PASSAGE COUNT
type: calc
func: set
param: passageCount
value: ''
format: ''
- label: RESET THE FRAME COUNT
type: calc
func: set
param: frameCount
value: ''
format: ''
- label: IMAGES PER GPT REQUEST
type: calc
func: set
param: batchSize
value: '10'
format: ''
- label: FRAME CEILING
type: calc
func: set
param: maxShots
value: '300'
format: ''
- label: INITIAL SETTLE DELAY
type: calc
func: set
param: settleDelay
value: '220'
format: ''
- label: MILLISECONDS BETWEEN FRAMES
type: calc
func: set
param: captureInterval
value: '520'
format: ''
- label: LAZY LOAD GRACE PERIOD
type: calc
func: set
param: growWait
value: '900'
format: ''
- label: DEFAULT START MODE
type: calc
func: set
param: startMode
value: top
format: ''
- label: DEFAULT PDF URL
type: calc
func: set
param: pdfUrl
value: '{{url}}'
format: ''
- label: CONFIGURE CAPTURE SCOPE
type: group
steps:
- label: SELECT CAPTURE SCOPE
type: ask
message: >-
How deep should this page be read?
One frame is one screenshot of one screen; between two frames the page
scrolls down by exactly one screen, and ten frames are read in one
vision request. The number is only a ceiling — the walk stops on its own
at the bottom of the page.
param: scopeChoice
options:
- label: 🔭 300 FRAMES · 299 scrolls · up to 30 requests · the deepest walk
value: '300'
- label: >-
📚 100 FRAMES · 99 scrolls · up to 10 requests · docs and long
threads
value: '100'
- label: 📄 50 FRAMES · 49 scrolls · up to 5 requests · a normal article
value: '50'
- label: 👀 20 FRAMES · 19 scrolls · up to 2 requests · a look at the top
value: '20'
- value: $custom
default: '300'
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: NORMALISE CAPTURE SCOPE
type: js
args: scopeChoice
code: >-
// FULL PAGE VISION · SCOPE
// Turns the chosen scope into the three numbers the run is actually
made of: how many
// screenshots may be taken, how many scroll steps that is, and how many
vision requests
// they can cost at ten images per request. Free text is accepted and
clamped, so a
// typed "1000" becomes the hard ceiling of 300 instead of an impossible
plan.
const HARD_MAX = 300;
const SIZE = 10;
const raw = String(args.scopeChoice == null ? '' : args.scopeChoice);
const digits = raw.match(/\d+/);
let frames = digits ? Number(digits[0]) : 300;
if (!isFinite(frames) || frames < 1) frames = 300;
frames = Math.min(HARD_MAX, Math.max(1, Math.round(frames)));
return {
frames: frames,
scrolls: Math.max(0, frames - 1),
requests: Math.ceil(frames / SIZE),
ceiling: HARD_MAX
};
param: scope
timeout: 30000
onFailure: ''
silent: true
- label: APPLY FRAME CEILING
type: calc
func: set
param: maxShots
value: '{{scope.frames}}'
format: ''
- label: NOTE THE CEILING
type: calc
func: set
param: scopeNote
value: ceiling {{scope.frames}} frames · max {{scope.requests}} requests
format: ''
- label: CONFIGURE START POINT
type: group
steps:
- label: SELECT START POINT
type: ask
message: Where should the walk begin?
param: startMode
options:
- label: ⬆️ TOP · photograph the page from the very beginning
value: top
- label: 📍 HERE · start at the current scroll position and walk down
value: here
default: top
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- condition: '{{startMode}} = here'
label: NOTE PARTIAL WALK
type: say
message: 📍 Starting at the current position — everything above it stays unread.
- label: CONFIGURE OUTPUT LANGUAGE
type: group
steps:
- label: SELECT OUTPUT LANGUAGE
type: ask
message: In which language should every answer be written?
param: languageChoice
options:
- label: 🔤 MATCH PAGE · answer in the language of the page itself
value: matchPage
- label: 🇩🇪 GERMAN · Deutsch
value: German
- label: 🇬🇧 ENGLISH · English
value: English
- label: 🇪🇸 SPANISH · Español
value: Spanish
- label: 🇫🇷 FRENCH · Français
value: French
- label: 🇸🇦 ARABIC · العربية
value: Arabic
- value: $custom
default: matchPage
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: APPLY SELECTED LANGUAGE
type: calc
func: set
param: outputLanguage
value: '{{languageChoice}}'
format: ''
- condition: '{{languageChoice}} = matchPage'
label: MATCH THE PAGE LANGUAGE
type: calc
func: set
param: outputLanguage
value: >-
the same language that the page itself is written in, as seen in the
screenshots
format: ''
- label: DETECT SOURCE TYPE
type: group
steps:
- label: MATCH PDF URL
type: calc
func: match
param: url
to: pdfMatch
regex: /https?:\/\/[^\s]+\.pdf(\/[^\s]*)?|[^\s]+\/pdf\/[^\s]*/
- label: ASSUME WEB PAGE
type: calc
func: set
param: sourceKind
value: html
format: ''
- condition: '{{pdfMatch}}'
label: SWITCH TO PDF
type: calc
func: set
param: sourceKind
value: pdf
format: ''
- condition: '{{pdfMatch}}'
label: STORE PDF URL
type: calc
func: set
param: pdfUrl
value: '{{pdfMatch}}'
format: ''
- condition: '{{sourceKind}} = html'
label: HTML CAPTURE PIPELINE
type: group
steps:
- label: ANNOUNCE HTML CAPTURE
type: say
message: 📸 Walking the page · {{scopeNote}}
- label: PREPARE CAPTURE
type: js
args: settleDelay, startMode
code: >-
// FULL PAGE VISION · PREPARE CAPTURE
// There is no separate preload pass: the capture walk itself scrolls
the page one
// viewport at a time, which is exactly what triggers lazy loading, so
loading and
// photographing happen in ONE sweep. This step neutralises animated
scrolling, finds
// the real scroll container, measures how much of the viewport is
permanently covered
// by pinned elements, parks the page at the chosen starting point and
publishes the
// capture state.
//
// STRIDE: exactly one screen, no artificial overlap. The only thing
subtracted is the
// height of elements that are pinned to the top or the bottom of the
screen. Those
// elements cover a strip of the viewport on every single frame, so
content that lands
// underneath them would never be photographed at all. Subtracting their
height is not
// overlap, it is the correction that makes an exact one-screen step
actually exact.
//
// Nothing in here is allowed to throw. Every failure is reported as
data.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
function killSmoothScrolling() {
// A page level `scroll-behavior: smooth` animates even behavior:'auto' and would
// leave every frame mid transition. Override it for the whole run.
try {
if (document.getElementById('__fpv_no_smooth')) return;
const style = document.createElement('style');
style.id = '__fpv_no_smooth';
style.textContent = 'html,body,:root,*{scroll-behavior:auto !important}';
(document.head || document.documentElement).appendChild(style);
} catch (e) { /* best effort */ }
}
function docScroller() {
return document.scrollingElement || document.documentElement || document.body;
}
function findInnerScroller() {
let best = null;
let bestScore = 0;
let nodes = [];
try {
nodes = document.querySelectorAll('div,main,section,article,ul,ol,table,form');
} catch (e) {
return null;
}
const limit = Math.min(nodes.length, 6000);
for (let i = 0; i < limit; i++) {
const el = nodes[i];
let sh = 0;
let ch = 0;
try { sh = el.scrollHeight; ch = el.clientHeight; } catch (e) { continue; }
if (sh <= ch + 200) continue;
let rect;
try { rect = el.getBoundingClientRect(); } catch (e) { continue; }
if (rect.width < 240 || rect.height < 240) continue;
let overflow = '';
try { overflow = window.getComputedStyle(el).overflowY; } catch (e) { overflow = ''; }
if (!/(auto|scroll|overlay)/.test(overflow)) continue;
const score = (sh - ch) * rect.width * rect.height;
if (score > bestScore) { bestScore = score; best = el; }
}
return best;
}
function pickScroller() {
const se = docScroller();
let sh = 0;
let ch = 0;
try { sh = Math.max(se.scrollHeight, document.body ? document.body.scrollHeight : 0); } catch (e) { sh = 0; }
try { ch = se.clientHeight || window.innerHeight || 800; } catch (e) { ch = window.innerHeight || 800; }
if (sh > ch + 120) return null; // null means: scroll the window
return findInnerScroller(); // may still be null
}
// How many pixels at the top and at the bottom of the screen are
permanently covered by
// pinned bars: navigation, cookie banners, sticky footers, chat
bubbles. Content that lands
// underneath them would never be photographed, so their height has to
come off the step.
//
// Measured by asking the browser what is actually painted at nine
points along each edge.
// The obvious alternative - walking every element and reading its
computed style - costs
// thousands of forced layouts on a long encyclopedia page and can take
longer than the walk
// itself. Eighteen hit tests answer the same question in constant time.
function measureOcclusion(vh, vw) {
const out = { top: 0, bottom: 0, items: 0 };
const xs = [Math.round(vw * 0.15), Math.round(vw * 0.5), Math.round(vw * 0.85)];
const topYs = [2, 12, 28];
const bottomYs = [vh - 2, vh - 12, vh - 28];
function stack(x, y) {
try { return document.elementsFromPoint(x, y) || []; } catch (e) { return []; }
}
function consider(el, edge, vh, vw) {
if (!el || el === document.body || el === document.documentElement) return;
let cs;
try { cs = window.getComputedStyle(el); } catch (e) { return; }
if (!cs) return;
if (cs.position !== 'fixed' && cs.position !== 'sticky') return;
if (cs.visibility === 'hidden' || cs.display === 'none' || Number(cs.opacity) === 0) return;
let r;
try { r = el.getBoundingClientRect(); } catch (e) { return; }
if (r.width < vw * 0.55) return;
if (r.height < 12 || r.height > vh * 0.6) return;
if (edge === 'top' && r.bottom > 4 && r.bottom < vh * 0.5) {
out.top = Math.max(out.top, Math.ceil(r.bottom));
out.items++;
} else if (edge === 'bottom' && r.top < vh - 4 && r.top > vh * 0.5) {
out.bottom = Math.max(out.bottom, Math.ceil(vh - r.top));
out.items++;
}
}
for (let i = 0; i < xs.length; i++) {
for (let j = 0; j < topYs.length; j++) {
const s = stack(xs[i], topYs[j]);
for (let k = 0; k < Math.min(s.length, 6); k++) consider(s[k], 'top', vh, vw);
}
for (let j = 0; j < bottomYs.length; j++) {
const s = stack(xs[i], bottomYs[j]);
for (let k = 0; k < Math.min(s.length, 6); k++) consider(s[k], 'bottom', vh, vw);
}
}
// A full screen overlay must not collapse the step to nothing.
const cap = Math.floor(vh * 0.4);
if (out.top + out.bottom > cap) {
const scale = cap / (out.top + out.bottom);
out.top = Math.floor(out.top * scale);
out.bottom = Math.floor(out.bottom * scale);
}
return out;
}
function heightOf(el) {
if (el) return el.scrollHeight;
const se = docScroller();
return Math.max(
se ? se.scrollHeight : 0,
document.body ? document.body.scrollHeight : 0,
document.documentElement ? document.documentElement.scrollHeight : 0,
window.innerHeight || 800
);
}
function viewportOf(el) {
if (el) return Math.max(200, el.clientHeight || 800);
return Math.max(200, window.innerHeight || 800);
}
function posOf(el) {
if (el) return el.scrollTop || 0;
const se = docScroller();
const w = typeof window.pageYOffset === 'number' ? window.pageYOffset : 0;
return Math.max(w, se ? (se.scrollTop || 0) : 0, document.body ? (document.body.scrollTop || 0) : 0);
}
function setPos(el, y) {
const target = Math.max(0, Math.round(y));
if (el) { el.scrollTop = target; return; }
try { window.scrollTo({ top: target, behavior: 'instant' }); } catch (e) { /* ignore */ }
try { window.scrollTo({ top: target, behavior: 'auto' }); } catch (e) { /* ignore */ }
try { window.scroll(0, target); } catch (e) { /* ignore */ }
const se = docScroller();
if (se) se.scrollTop = target;
if (document.body) document.body.scrollTop = target;
}
// Deliberately no frame prediction. The height measured here is the
height of the page
// BEFORE the walk, and the walk is what makes a lazy page load the rest
of itself.
const result = {
ok: 'no', viewport: 0, total: 0, stride: 0, startY: 0,
pinnedTop: 0, pinnedBottom: 0, pinned: 0,
scroller: 'window', start: 'top', error: ''
};
try {
const wantHere = String(args.startMode || 'top').toLowerCase().indexOf('here') === 0;
killSmoothScrolling();
const el = pickScroller();
result.scroller = el ? 'element' : 'window';
result.start = wantHere ? 'here' : 'top';
const keepAt = wantHere ? posOf(el) : 0;
setPos(el, keepAt);
await sleep(Math.max(120, Number(args.settleDelay) || 250));
const vh = viewportOf(el);
const vw = Math.max(320, window.innerWidth || 1280);
const height = heightOf(el);
const occ = measureOcclusion(vh, vw);
const stride = Math.max(120, vh - occ.top - occ.bottom);
const startY = posOf(el);
window.__fpv = {
el: el, stride: stride, viewport: vh, total: height,
frames: 0, startY: startY, abort: false, misses: 0, retries: 0,
pinnedTop: occ.top, pinnedBottom: occ.bottom
};
result.viewport = vh;
result.total = height;
result.stride = stride;
result.startY = startY;
result.pinnedTop = occ.top;
result.pinnedBottom = occ.bottom;
result.pinned = occ.items;
result.ok = 'yes';
} catch (e) {
result.error = String((e && e.message) || e);
}
return result;
param: capture
timeout: 60000
onFailure: PLAN FRAME LIST
silent: true
- label: PLAN FRAME LIST
type: js
args: maxShots
code: >-
// FULL PAGE VISION · FRAME LIST
// The walk cannot know the final page height in advance, because lazy
content only
// appears while scrolling. The list is therefore the ceiling chosen by
the user,
// not a prediction: the loop leaves early the moment the scroller stops
moving.
const MAX_SHOTS = Math.max(1, Math.min(300, Number(String(args.maxShots
|| '').replace(/[^0-9]/g, '')) || 300));
const list = [];
for (let i = 1; i <= MAX_SHOTS; i++) list.push({ n: i });
return list;
param: frameList
timeout: 30000
onFailure: FINISH CAPTURE
silent: true
- label: CAPTURE HTML FRAMES
type: loop
list: frameList
steps:
- label: APPEND FRAME
type: js
args: shots, view, maxShots, capturedFrames
code: >-
// FULL PAGE VISION · APPEND FRAME
// Exactly the accumulation of the original command, which is the
only form proven
// to deliver several images to one prompt:
//
// arrayValue += ',' + viewValue -> a real parameter -> used
in the prompt
//
// Two rules follow from that and must not be touched:
// * the separator is a COMMA. HARPA splits the stored series on
commas to resolve
// the individual attachments. A newline joined series arrives
as a single blob
// and only one image reaches the model.
// * the token travels as a JS ARGUMENT. Interpolating {{view}}
inside a calc step
// coerces the reference to text and breaks it, which surfaces
as
// "Failed to execute 'createImageBitmap'".
//
// Retry behaviour is built here, not delegated: a missing frame
throws on purpose
// so the loop lands on the pacing step, waits, and retries the SAME
scroll position
// on the next pass. The counter step is skipped on that path, so
the frame count
// stays exact. After too many refusals in a row the pacing step
raises the abort
// flag and this step stops throwing, which lets the walk end
cleanly instead of
// grinding through the whole ceiling.
const LIMIT = Math.max(1, Math.min(300, Number(String(args.maxShots
|| '').replace(/[^0-9]/g, '')) || 300));
const held = Math.max(0, Number(args.capturedFrames) || 0);
let arrayValue = args['shots'] || '';
const viewValue = args.view;
const S = window.__fpv || (window.__fpv = {});
if (S.abort === true) return arrayValue; // clean shutdown, no
further frames
if (held >= LIMIT) return arrayValue; // ceiling reached
if (!viewValue) {
S.misses = (Number(S.misses) || 0) + 1;
throw new Error('frame not available yet');
}
S.misses = 0;
if (arrayValue === '') {
arrayValue = viewValue;
} else {
arrayValue += ',' + viewValue;
}
return arrayValue;
param: shots
timeout: 60000
onFailure: PACE CAPTURE
silent: true
- label: COUNT CAPTURED FRAME
type: calc
func: increment
param: capturedFrames
delta: '1'
- label: ADVANCE ONE FRAME
type: js
args: captureInterval, growWait
code: >-
// FULL PAGE VISION · ADVANCE ONE FRAME
// Runs immediately after a frame was grabbed. Scrolls forward by
exactly one stride -
// one full screen minus whatever is pinned to the top or bottom
edge, so the frames sit
// edge to edge without overlap and without a hidden gap -
// verifies the real position, gives lazily loaded content the
chance to extend the
// page, and returns true only when the scroller truly cannot move
any further.
// Loading and capturing therefore happen in a single downward pass.
//
// It is also the single exit of the walk: when the pacing step has
given up on a
// position, the abort flag is read here and reported as "bottom
reached", so the
// existing exit jump ends the loop without any extra control flow.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
function docScroller() {
return document.scrollingElement || document.documentElement || document.body;
}
function heightOf(el) {
if (el) return el.scrollHeight;
const se = docScroller();
return Math.max(
se ? se.scrollHeight : 0,
document.body ? document.body.scrollHeight : 0,
document.documentElement ? document.documentElement.scrollHeight : 0,
window.innerHeight || 800
);
}
function viewportOf(el) {
if (el) return Math.max(200, el.clientHeight || 800);
return Math.max(200, window.innerHeight || 800);
}
function posOf(el) {
if (el) return el.scrollTop || 0;
const se = docScroller();
const w = typeof window.pageYOffset === 'number' ? window.pageYOffset : 0;
return Math.max(w, se ? (se.scrollTop || 0) : 0, document.body ? (document.body.scrollTop || 0) : 0);
}
function setPos(el, y) {
const target = Math.max(0, Math.round(y));
if (el) { el.scrollTop = target; return; }
try { window.scrollTo({ top: target, behavior: 'instant' }); } catch (e) { /* ignore */ }
try { window.scrollTo({ top: target, behavior: 'auto' }); } catch (e) { /* ignore */ }
try { window.scroll(0, target); } catch (e) { /* ignore */ }
const se = docScroller();
if (se) se.scrollTop = target;
if (document.body) document.body.scrollTop = target;
}
const START = Date.now();
const rawInterval = String(args.captureInterval == null ? '' :
args.captureInterval).replace(/[^0-9]/g, '');
const INTERVAL = Math.max(0, Math.min(5000, Number(rawInterval) ||
200));
const SETTLE = Math.max(30, Math.min(300, Math.round(INTERVAL *
0.45) || 90));
const GROW_WAIT = Math.max(0, Number(args.growWait) || 900);
let state = window.__fpv;
if (!state || typeof state !== 'object') {
const vh = viewportOf(null);
state = { el: null, stride: vh, viewport: vh, total: heightOf(null), frames: 0, startY: 0, abort: false };
window.__fpv = state;
}
if (state.abort === true) return true;
if (state.el && !document.contains(state.el)) state.el = null;
const el = state.el || null;
const stride = Math.max(120, Number(state.stride) ||
viewportOf(el));
try {
state.lastShot = START;
state.frames = (Number(state.frames) || 0) + 1;
const before = posOf(el);
const heightBefore = heightOf(el);
const want = before + stride;
setPos(el, want);
await sleep(SETTLE);
let after = posOf(el);
// The scroller did not move although there is room left: it may still be animating
// or re-rendering. Push once more before drawing any conclusion.
if (after - before < 8 && before < heightOf(el) - viewportOf(el) - 8) {
setPos(el, want);
await sleep(Math.max(240, SETTLE));
after = posOf(el);
}
// We look like we are at the bottom. Lazy loaders usually append content only once
// the bottom is actually reached, so wait for the page to grow.
if (after - before < 8 && GROW_WAIT > 0) {
const deadline = Date.now() + GROW_WAIT;
while (Date.now() < deadline) {
await sleep(150);
if (heightOf(el) > heightBefore + 8) {
setPos(el, want);
await sleep(SETTLE);
after = posOf(el);
if (after - before >= 8) break;
}
}
}
state.total = heightOf(el);
state.viewport = viewportOf(el);
return (after - before) < 8;
} catch (e) {
return true;
}
param: atBottom
timeout: 60000
onFailure: ''
silent: true
- condition: '{{atBottom}} = true'
label: EXIT AT PAGE BOTTOM
type: jump
to: FINISH CAPTURE
- label: PACE CAPTURE
type: js
args: captureInterval
code: >-
// FULL PAGE VISION · PACE CAPTURE
// Last step of the capture loop, and therefore also the landing
point when a frame
// grab fails. Three jobs:
// 1. keep the requested capture rate without paying twice for
time already spent
// scrolling;
// 2. back off progressively when frames are refused, so a
throttled run recovers
// instead of spinning through the loop budget;
// 3. give up in a controlled way. After GIVE_UP consecutive
refusals of the same
// position the abort flag is raised. The append step then
stops throwing and
// the scroll step reports "bottom", so the walk ends with
whatever it has
// instead of retrying three hundred times.
// Chrome enforces its own screenshot rate limit of two calls per
second and it
// cannot be raised; this step only controls how fast the command
asks for frames.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
const GIVE_UP = 6;
const raw = String(args.captureInterval == null ? '' :
args.captureInterval).replace(/[^0-9]/g, '');
const interval = Math.max(0, Math.min(5000, Number(raw) || 200));
const S = window.__fpv || (window.__fpv = {});
const advanced = Number(S.frames) || 0;
// The scroll step increments S.frames. If it did not run since the
last pause, the
// previous capture was refused and this is a retry of the same
position.
if (S.lastAdvance === advanced) {
S.retries = (Number(S.retries) || 0) + 1;
} else {
S.retries = 0;
S.lastAdvance = advanced;
}
const retries = Number(S.retries) || 0;
if (retries >= GIVE_UP) S.abort = true;
const backoff = retries > 0 ? Math.min(4000, 300 * Math.pow(2,
retries - 1)) : 0;
const since = S.lastShot ? Date.now() - Number(S.lastShot) :
interval;
const waitFor = Math.max(0, interval - since) + backoff;
if (waitFor > 0) await sleep(waitFor);
return { waited: waitFor, retries: retries, interval: interval,
frames: advanced, abort: S.abort === true ? 'yes' : 'no' };
param: scratch
timeout: 30000
onFailure: ''
silent: true
- label: AFTER HTML CAPTURE
type: jump
to: FINISH CAPTURE
- condition: '{{sourceKind}} = pdf'
label: PDF CAPTURE PIPELINE
type: group
steps:
- label: ANNOUNCE PDF CAPTURE
type: say
message: >-
📄 PDF · {{scopeNote}} · reading the page count, then photographing
every page.
- label: PROBE PDF PAGE COUNT
type: js
args: ''
code: >-
// FULL PAGE VISION · PROBE PDF PAGE COUNT
// Asks the viewer how many pages the document has, for free, before a
vision request is spent
// on the same question.
//
// Why this matters beyond the saved request: when the page count is
unknown the plan falls
// back to the chosen ceiling, and the ceiling defaults to three
hundred. On a twelve page PDF
// that meant three hundred navigations to pages that do not exist,
three hundred photographs
// of the same last page, and thirty vision requests spent reading it. A
number that is right
// removes the whole failure mode, and the viewers hand it over if
asked.
//
// Four sources, cheapest and most reliable first. Every one of them is
wrapped, because a
// missing viewer must cost nothing but a zero.
function deepQuery(root, selector, depth) {
if (!root || depth > 6) return null;
try {
const direct = root.querySelector ? root.querySelector(selector) : null;
if (direct) return direct;
} catch (e) { /* keep looking */ }
let kids = [];
try { kids = root.querySelectorAll ? Array.prototype.slice.call(root.querySelectorAll('*')) : []; } catch (e) { kids = []; }
for (let i = 0; i < Math.min(kids.length, 400); i++) {
const sr = kids[i].shadowRoot;
if (!sr) continue;
const hit = deepQuery(sr, selector, depth + 1);
if (hit) return hit;
}
return null;
}
function digits(value) {
const m = String(value == null ? '' : value).match(/\d[\d.,\u00a0 ]*/);
if (!m) return 0;
const n = Number(m[0].replace(/[^0-9]/g, ''));
return isFinite(n) ? n : 0;
}
const out = { pages: 0, pagesText: '', source: 'none', base: '' };
// The base URL the page loop navigates against. A PDF opened at a
bookmark already carries a
// fragment, and appending a second #page= to it produces an address no
viewer understands.
try {
const href = String(window.location.href || '');
out.base = href.split('#')[0];
} catch (e) { out.base = ''; }
// 1 · PDF.js and every viewer built on it publish the count directly.
try {
const app = window.PDFViewerApplication;
if (app && Number(app.pagesCount) > 0) {
out.pages = Number(app.pagesCount);
out.source = 'pdfjs';
}
} catch (e) { /* not a PDF.js viewer */ }
// 2 · The PDF.js toolbar prints it as plain text.
if (out.pages < 1) {
try {
const el = document.getElementById('numPages') || document.querySelector('#numPages,.numPages,[data-l10n-id="page_of_pages"]');
const n = el ? digits(el.textContent) : 0;
if (n > 0) { out.pages = n; out.source = 'toolbar'; }
} catch (e) { /* no toolbar */ }
}
// 3 · Chrome's built in viewer keeps its toolbar inside nested shadow
roots.
if (out.pages < 1) {
try {
const el = deepQuery(document, '#pagelength, viewer-page-selector #pagelength, #pageselector', 0);
const n = el ? digits(el.textContent) : 0;
if (n > 0) { out.pages = n; out.source = 'shadow'; }
} catch (e) { /* no shadow toolbar */ }
}
// 4 · Last free resort: the visible "7 / 34" the viewers print
somewhere on screen.
if (out.pages < 1) {
try {
const text = String(document.body ? document.body.innerText || '' : '').slice(0, 4000);
const m = text.match(/(?:^|\s)(\d{1,4})\s*(?:\/|of|von|sur|de)\s*(\d{1,4})(?:\s|$)/i);
const n = m ? Number(m[2]) : 0;
if (n > 0 && n <= 5000) { out.pages = n; out.source = 'text'; }
} catch (e) { /* nothing readable */ }
}
if (!isFinite(out.pages) || out.pages < 1 || out.pages > 5000) {
out.pages = 0;
out.source = 'none';
}
// The reading ladder downstream is gated on one slot being empty. A
probe that found nothing
// therefore has to hand back an EMPTY string rather than a zero,
because "0" is not empty and
// would silently skip the vision fallback it is supposed to trigger.
out.pagesText = out.pages > 0 ? String(out.pages) : '';
return out;
param: pdfProbe
timeout: 30000
onFailure: ''
silent: true
- condition: '{{pdfProbe.base}}'
label: ADOPT PDF BASE URL
type: calc
func: set
param: pdfUrl
value: '{{pdfProbe.base}}'
format: ''
- condition: '{{pdfProbe.pages}} > 0'
label: NOTE VIEWER PAGE COUNT
type: say
message: >-
📄 The viewer reports **{{pdfProbe.pages}}** page(s) — no request was
needed to find that out.
- label: SEED PDF PAGE COUNT
type: calc
func: set
param: pdfPageRead
value: '{{pdfProbe.pagesText}}'
format: ''
- condition: '{{pdfPageRead}} ='
label: READ PDF PAGE COUNT
type: gpt
prompt: >-
Analyze the provided screenshot of a PDF document and determine the
total number of pages. **Provide *ONLY* the numeral** representing the
page count as your output. Ensure the number is **strictly unformatted**
(no backticks, symbols, punctuation, or additional text of any kind).
Your response must be **exclusively the numeral** (e.g., `5`, `12`,
`23`) and *nothing else*:
{{view}}
param: pdfPageRead
isolated: true
silent: true
- condition: '{{pdfPageRead}} ='
label: RETRY PDF PAGE COUNT
type: gpt
prompt: >-
Look at the attached screenshot of a PDF and answer with the total
number of pages in the document — the number printed after the page
indicator, for example the 34 in `7 / 34`.
Answer with that numeral alone. No words, no punctuation, no formatting.
{{view}}
param: pdfPageRead
isolated: true
silent: true
- condition: '{{pdfPageRead}} ='
label: NOTE PDF PAGE COUNT FALLBACK
type: say
message: >-
🧭 Neither the viewer nor the vision reading could name the page count,
so the first **20** pages are photographed. That bound is deliberate:
without a real count the walk would otherwise navigate to hundreds of
pages that do not exist and photograph the last one over and over.
- label: PLAN PDF PAGES
type: js
args: pdfPageRead, maxShots, pdfProbe
code: >-
// FULL PAGE VISION · PDF SHOT PLAN
// One screenshot per PDF page. Three sources for the page count, in
falling order of trust:
//
// 1. the viewer itself, read out of the DOM one step earlier - free
and exact;
// 2. a vision reading of the first frame - one request, and only when
the viewer said
// nothing;
// 3. a blind fallback, because a run that cannot count still has to
produce something.
//
// The blind fallback is deliberately NOT the chosen ceiling. The
ceiling defaults to three
// hundred, and walking three hundred pages of a document that has
twelve means three hundred
// navigations, three hundred photographs of the same clamped last page,
and thirty vision
// requests spent reading that same page over and over. Where the count
is genuinely unknown
// the plan stops at BLIND_LIMIT pages and the command says so in plain
words: a bounded,
// announced partial reading instead of an unbounded silent one.
const MAX_SHOTS = Math.max(1, Math.min(300, Number(String(args.maxShots
|| '').replace(/[^0-9]/g, '')) || 300));
const BLIND_LIMIT = 20;
let probeSource = 'none';
try {
const p = args.pdfProbe;
if (p && typeof p === 'object' && p.source) probeSource = String(p.source);
} catch (e) { probeSource = 'none'; }
const raw = String(args.pdfPageRead == null ? '' : args.pdfPageRead);
const seen = raw.match(/\d+/);
const counted = seen ? Number(seen[0]) : 0;
let pages;
let source;
if (isFinite(counted) && counted > 0) {
pages = counted;
source = probeSource !== 'none' ? 'viewer' : 'vision';
} else {
pages = Math.min(MAX_SHOTS, BLIND_LIMIT);
source = 'blind';
}
pages = Math.max(1, Math.min(pages, MAX_SHOTS));
const plan = [];
for (let i = 1; i <= pages; i++) plan.push({ n: i, page: i, source:
source });
return plan;
param: frameList
timeout: 30000
onFailure: FINISH CAPTURE
silent: true
- label: CAPTURE PDF PAGES
type: loop
list: frameList
steps:
- label: OPEN PDF PAGE
type: navigate
url: '{{pdfUrl}}#page={{item.page}}'
waitForIdle: true
onFailure: ''
silent: true
- label: SETTLE PDF PAGE
type: wait
for: custom-delay
delay: 400
silent: true
- label: APPEND PDF FRAME
type: js
args: shots, view, maxShots, capturedFrames
code: >-
// FULL PAGE VISION · APPEND FRAME
// Exactly the accumulation of the original command, which is the
only form proven
// to deliver several images to one prompt:
//
// arrayValue += ',' + viewValue -> a real parameter -> used
in the prompt
//
// Two rules follow from that and must not be touched:
// * the separator is a COMMA. HARPA splits the stored series on
commas to resolve
// the individual attachments. A newline joined series arrives
as a single blob
// and only one image reaches the model.
// * the token travels as a JS ARGUMENT. Interpolating {{view}}
inside a calc step
// coerces the reference to text and breaks it, which surfaces
as
// "Failed to execute 'createImageBitmap'".
//
// Retry behaviour is built here, not delegated: a missing frame
throws on purpose
// so the loop lands on the pacing step, waits, and retries the SAME
scroll position
// on the next pass. The counter step is skipped on that path, so
the frame count
// stays exact. After too many refusals in a row the pacing step
raises the abort
// flag and this step stops throwing, which lets the walk end
cleanly instead of
// grinding through the whole ceiling.
const LIMIT = Math.max(1, Math.min(300, Number(String(args.maxShots
|| '').replace(/[^0-9]/g, '')) || 300));
const held = Math.max(0, Number(args.capturedFrames) || 0);
let arrayValue = args['shots'] || '';
const viewValue = args.view;
const S = window.__fpv || (window.__fpv = {});
if (S.abort === true) return arrayValue; // clean shutdown, no
further frames
if (held >= LIMIT) return arrayValue; // ceiling reached
if (!viewValue) {
S.misses = (Number(S.misses) || 0) + 1;
throw new Error('frame not available yet');
}
S.misses = 0;
if (arrayValue === '') {
arrayValue = viewValue;
} else {
arrayValue += ',' + viewValue;
}
return arrayValue;
param: shots
timeout: 60000
onFailure: PACE PDF CAPTURE
silent: true
- label: COUNT CAPTURED PDF FRAME
type: calc
func: increment
param: capturedFrames
delta: '1'
- label: PACE PDF CAPTURE
type: js
args: captureInterval, capturedFrames
code: >-
// FULL PAGE VISION · PACE PDF CAPTURE
// Last step of the PDF loop, and therefore also the landing point
when a page grab fails.
// Three jobs: keep the requested capture rate, back off
progressively when pages are refused,
// and give up in a controlled way after GIVE_UP consecutive
refusals of the same page.
//
// It used to detect a refusal by watching window.__fpv.frames, the
counter the HTML walk's
// scroll step increments. The PDF pipeline has no scroll step, so
that counter never moved:
// from the second page onwards every pass looked like a refusal of
the same position, the
// backoff doubled, and after six pages the abort flag went up and
the append step stopped
// appending. A PDF longer than seven pages silently lost everything
after page seven.
//
// The honest signal is the frame counter of the run itself. It is
incremented by the step
// that follows a SUCCESSFUL append and is skipped when the append
throws, so an unchanged
// value means, precisely and only, that the previous page was
refused.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
const GIVE_UP = 6;
const raw = String(args.captureInterval == null ? '' :
args.captureInterval).replace(/[^0-9]/g, '');
const interval = Math.max(0, Math.min(5000, Number(raw) || 200));
const captured = Math.max(0, Number(String(args.capturedFrames ==
null ? '' : args.capturedFrames).replace(/[^0-9]/g, '')) || 0);
const S = window.__fpv || (window.__fpv = {});
if (S.lastPdfCount === captured) {
S.pdfRetries = (Number(S.pdfRetries) || 0) + 1;
} else {
S.pdfRetries = 0;
S.lastPdfCount = captured;
}
const retries = Number(S.pdfRetries) || 0;
if (retries >= GIVE_UP) S.abort = true;
const backoff = retries > 0 ? Math.min(4000, 300 * Math.pow(2,
retries - 1)) : 0;
const since = S.lastShot ? Date.now() - Number(S.lastShot) :
interval;
const waitFor = Math.max(0, interval - since) + backoff;
if (waitFor > 0) await sleep(waitFor);
S.lastShot = Date.now();
return { waited: waitFor, retries: retries, interval: interval,
pages: captured, abort: S.abort === true ? 'yes' : 'no' };
param: scratch
timeout: 30000
onFailure: ''
silent: true
- label: RETURN TO FIRST PAGE
type: navigate
url: '{{pdfUrl}}'
waitForIdle: false
onFailure: ''
silent: true
- label: AFTER PDF CAPTURE
type: jump
to: FINISH CAPTURE
- label: FINALIZE CAPTURE
type: group
steps:
- label: FINISH CAPTURE
type: js
args: sourceKind
code: >-
// FULL PAGE VISION · FINISH CAPTURE
// Returns the page to where the walk began and reports the geometry
that was really walked,
// which is the fastest way to tell whether a run covered the page.
Coverage is measured
// against the part of the page that was in scope, so a walk that
deliberately started half
// way down is not reported as half a failure.
//
// A PDF has no walk. Its frames come from navigation, so window.__fpv
is never set up and
// every measurement below reads back as zero - which used to make every
single PDF run end
// on "only 0 % of the page height was walked" plus a diagnostics line
of zeroes. Coverage is
// not a meaningful question there: the plan names the pages and the
loop photographs them.
// So the mode is named, the geometry is reported as complete, and the
warnings that hang off
// coverage simply do not fire.
function docScroller() {
return document.scrollingElement || document.documentElement || document.body;
}
const kind = String(args.sourceKind ||
'html').toLowerCase().indexOf('pdf') === 0 ? 'pdf' : 'html';
const state = window.__fpv || {};
const el = (state.el && document.contains(state.el)) ? state.el : null;
const out = {
ok: 'true',
mode: kind,
total: Number(state.total) || 0,
viewport: Number(state.viewport) || (window.innerHeight || 0),
stride: Number(state.stride) || 0,
walked: Number(state.frames) || 0,
startY: Number(state.startY) || 0,
aborted: state.abort === true ? 'yes' : 'no',
coverage: 0,
scroller: el ? 'element' : 'window'
};
if (kind === 'pdf') {
out.coverage = 100;
out.scroller = 'document';
return out;
}
try {
const home = out.startY;
if (el) {
el.scrollTop = home;
} else {
try { window.scrollTo({ top: home, behavior: 'instant' }); } catch (e) { /* ignore */ }
try { window.scrollTo({ top: home, behavior: 'auto' }); } catch (e) { /* ignore */ }
try { window.scroll(0, home); } catch (e) { /* ignore */ }
const se = docScroller();
if (se) se.scrollTop = home;
if (document.body) document.body.scrollTop = home;
}
const inScope = Math.max(1, out.total - out.startY);
const covered = out.stride > 0 && out.walked > 0
? (out.walked - 1) * out.stride + out.viewport
: 0;
out.coverage = Math.max(0, Math.min(100, Math.round(100 * covered / inScope)));
await new Promise(function (r) { setTimeout(r, 150); });
} catch (e) {
out.ok = 'false';
}
return out;
param: geometry
timeout: 30000
onFailure: ''
silent: true
- label: MEASURE FRAME SERIES
type: js
args: shots, capturedFrames
code: >-
// FULL PAGE VISION · MEASURE FRAME SERIES
// The single source of truth for "how many frames do we actually hold".
The engine
// side counter can drift by one when the walk shuts itself down, so the
number is
// recomputed from the stored series instead of being trusted.
//
// A frame token is opaque. Should it contain a comma of its own, a
naive split would
// tear it apart and multiply the count. Two independent reconstructions
are used:
// 1. if the tokens are data URLs, the boundaries are visible and
exact;
// 2. otherwise, if the number of comma parts is an exact multiple of
the counted
// frames, that multiple is the number of parts per token;
// 3. otherwise one part is one token.
const raw = String(args.shots == null ? '' : args.shots);
const counted = Math.max(0, Number(args.capturedFrames) || 0);
const parts = raw
.split(',')
.map(function (s) { return s.trim(); })
.filter(function (s) { return s.length > 0; });
let frames = [];
let method = 'one-part-per-frame';
const looksLikeDataUrl = parts.some(function (p) { return
/^data:/i.test(p); });
if (counted > 0 && parts.length > counted && parts.length % counted ===
0) {
method = 'derived-parts-per-frame';
const per = parts.length / counted;
for (let i = 0; i < parts.length; i += per) frames.push(parts.slice(i, i + per).join(','));
} else if (looksLikeDataUrl) {
method = 'data-url-boundaries';
let current = [];
for (let i = 0; i < parts.length; i++) {
if (/^data:/i.test(parts[i]) && current.length > 0) {
frames.push(current.join(','));
current = [];
}
current.push(parts[i]);
}
if (current.length > 0) frames.push(current.join(','));
} else {
frames = parts.slice();
}
// A request that carries ten dense screenshots takes roughly twenty to
fifty seconds. Saying
// so at the gate is what separates "this is working" from "this has
hung", and it costs
// nothing to say.
const size = 10;
const requests = Math.ceil(frames.length / size);
return {
frames: frames.length,
parts: parts.length,
counted: counted,
requests: requests,
minLo: Math.max(1, Math.round(requests * 20 / 60)),
minHi: Math.max(2, Math.round(requests * 50 / 60)),
method: method,
ok: frames.length > 0 ? 'yes' : 'no'
};
param: series
timeout: 30000
onFailure: ''
silent: true
- label: ADOPT FRAME COUNT
type: calc
func: set
param: shotCount
value: '{{series.frames}}'
format: ''
- label: SPLIT INTO BATCHES
type: js
args: shots, batchSize, shotCount
code: >-
// FULL PAGE VISION · BATCH SPLITTER
// A chat request carries up to ten images, so the comma separated frame
series is cut
// into groups of ten and each group is read by its own vision call. The
token
// boundaries are reconstructed exactly the way the measuring step
reconstructs them,
// so both steps always agree on what a frame is.
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) || 10));
const counted = Math.max(0, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 0);
const raw = String(args.shots == null ? '' : args.shots);
const parts = raw
.split(',')
.map(function (s) { return s.trim(); })
.filter(function (s) { return s.length > 0; });
// Order matters and follows the original: the engine side counter says
how many frames were
// really taken, so the number of comma parts each token occupies is
DERIVED from it. Only when
// that derivation does not apply is the visible shape of the token used
as a fallback.
let frames = [];
const looksLikeDataUrl = parts.some(function (p) { return
/^data:/i.test(p); });
if (counted > 0 && parts.length > counted && parts.length % counted ===
0) {
const per = parts.length / counted;
for (let i = 0; i < parts.length; i += per) frames.push(parts.slice(i, i + per).join(','));
} else if (looksLikeDataUrl) {
let current = [];
for (let i = 0; i < parts.length; i++) {
if (/^data:/i.test(parts[i]) && current.length > 0) {
frames.push(current.join(','));
current = [];
}
current.push(parts[i]);
}
if (current.length > 0) frames.push(current.join(','));
} else {
frames = parts.slice();
}
const batches = [];
for (let i = 0; i < frames.length; i += SIZE) {
batches.push(frames.slice(i, i + SIZE).join(','));
}
return batches;
param: batches
timeout: 30000
onFailure: ''
silent: true
- label: BUILD PROMPT HEADER
type: js
args: title, url, shotCount, batches
code: >-
// FULL PAGE VISION · PROMPT HEADER
// The three lines that sit above every answer prompt. Two of them - the
page title and the
// address - come from the page itself, and page text is exactly what
has to be armoured
// before it is interpolated into a prompt: a title that reads "How
{{page}} works" would
// otherwise make the engine extract the whole page again, once per
occurrence, for every
// answer that carries the header. The record has been armoured since
the first version; the
// header had not been, and it is built from the same kind of untrusted
text.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
function clean(value) {
return harden(String(value == null ? '' : value).replace(/\s+/g, ' ').trim());
}
const title = clean(args.title) || 'untitled page';
const url = clean(args.url) || 'unknown source';
const frames = String(args.shotCount == null ? '' :
args.shotCount).replace(/[^0-9]/g, '') || '0';
let segments = 0;
try {
const b = args.batches;
segments = b && typeof b.length === 'number' ? b.length : 0;
} catch (e) { segments = 0; }
return [
'[PAGE TITLE]: ' + title,
'[SOURCE]: ' + url,
'[FRAMES READ]: ' + frames + ' screenshots in ' + segments + ' segment(s)'
].join('\n');
param: promptHeader
timeout: 30000
onFailure: ''
silent: true
- label: BUILD GROUND RULE
type: calc
func: set
param: groundRule
value: >-
The [EVIDENCE] below is the frame-by-frame reading of the whole page,
produced by looking at every screenshot. It is the only permitted source
of facts. Never add anything it does not contain. Regions the record
marks as `[unreadable]` or as `NOT READ` are gaps, not facts: name them
as missing instead of filling them in.
format: ''
- label: BUILD CONSTRUCTION RULE
type: calc
func: set
param: buildRule
value: >-
The [EVIDENCE] below is the frame-by-frame reading of the whole page,
written by looking at every screenshot. It is your only source for what
the page CONTAINS: its words, its numbers, its pictures, its structure,
and the typefaces, sizes, colours and spacing it describes.
It is not the source of the FORM your answer takes. When you are asked
to build something - HTML, CSS, code, a table, a document, a translation
- then writing the markup, the syntax and the scaffolding that carries
the content is construction, not invention. That is the work, and you
are expected to do it in full. Only the content inside it has to come
from the record.
format: ''
- label: REPORT CAPTURE
type: say
message: 🖼️ **{{shotCount}}** frames · **{{batches.length}}** segment(s)
- condition:
- '{{geometry.coverage}} < 95'
- '{{geometry.aborted}} = yes'
label: REPORT GEOMETRY
type: say
message: >-
🔎 Diagnostics ({{geometry.mode}}): {{geometry.total}} px tall,
{{geometry.viewport}} px viewport, exact step {{geometry.stride}} px
({{geometry.pinnedTop}} px pinned at the top, {{geometry.pinnedBottom}}
px at the bottom), scroller {{geometry.scroller}}, coverage
{{geometry.coverage}} %.
- condition: '{{geometry.coverage}} < 95'
label: WARN LOW COVERAGE
type: say
message: >-
⚠️ Only {{geometry.coverage}} % of the page height was walked. Frames
may have been refused by the browser — the record below can have holes.
- condition: '{{shotCount}} = {{maxShots}}'
label: WARN CEILING REACHED
type: say
message: >-
♾️ The chosen ceiling of {{maxShots}} frames was reached, so the source
probably continues beyond it. Run the command again with a larger scope
— on a web page you can also set START POINT to HERE to carry on from
where this run stopped.
- condition: '{{capture.ok}} = no'
label: WARN PREPARATION FAILED
type: say
message: >-
⚠️ The page could not be prepared for the walk ({{capture.error}}), so
the frames may be uneven. What was captured is still usable.
- condition: '{{geometry.aborted}} = yes'
label: WARN CAPTURE ABORTED
type: say
message: >-
🛑 The walk stopped early because the browser refused several frames in
a row. What was captured up to that point is intact.
- condition: '{{shotCount}} = 0'
label: ABORT NOTICE
type: say
message: >-
⛔ No frames could be captured. The connection may not support vision, or
this tab may refuse screenshots. Try a normal web page with a vision-capable
model.
- condition: '{{shotCount}} = 0'
label: STOP ON EMPTY CAPTURE
type: stop
- condition: '{{batches.length}} > 1'
label: CONFIRM VISION READING
type: group
steps:
- label: CONFIRM READING
type: ask
message: >-
Read all **{{shotCount}}** frames now? That is **{{batches.length}}**
vision request(s), roughly **{{series.minLo}}–{{series.minHi}}
minutes**. Every answer afterwards costs one more; the frames are read
only once.
param: proceed
options:
- label: ▶️ CONTINUE · read the frames
value: continue
- label: ⛔ CANCEL · stop without spending a request
value: cancel
default: continue
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- condition: '{{proceed}} = cancel'
label: CANCEL VISION READING
type: jump
to: READING CANCELLED
- condition: '{{batches.length}} = 1'
label: NOTE SINGLE REQUEST
type: say
message: 💳 One request covers this page.
- label: CONFIGURE READING INSTRUCTIONS
type: group
steps:
- label: SELECT READING INSTRUCTIONS
type: ask
message: >-
Any instructions for the reading?
SKIP reads the whole page in full detail. Anything you type takes
precedence over that, and is carried out on the finished record as well.
param: focusChoice
options:
- label: ⏭️ SKIP · read everything, in as much detail as possible
value: skip
- value: $custom
default: skip
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: BUILD READING INSTRUCTIONS
type: js
args: focusChoice
code: >-
// FULL PAGE VISION · READING INSTRUCTIONS
// One answer, given before a single frame is read, and it is an
INSTRUCTION rather than a
// choice from a list. There is nothing to pick because there is nothing
to narrow down: the
// reading always extracts everything it can see, in the greatest detail
it can manage. That
// is the floor, not an option.
//
// SKIP means exactly that floor and nothing added.
//
// Anything typed keeps the whole floor - the same completeness, the
same detail, the same
// record format - and is laid on top of it with PRECEDENCE. Precedence
is the point and it
// cuts both ways:
// * "rebuild this page one to one, design and fonts included" makes
the reading write down
// typefaces, sizes, colours and spacing it would otherwise only
have implied;
// * "do not extract the images, text only" makes it stop describing
pictures, and that is
// allowed to override the format above, because the person reading
the record said so.
//
// This is why the instruction is asked HERE and not after the reading.
The reading is the
// only step that ever sees the pixels. Everything downstream works from
the words it wrote,
// and a picture that was never described cannot be described later,
however precisely it is
// asked for.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const raw = String(args.focusChoice == null ? '' : args.focusChoice);
const flat = raw.replace(/\s+/g, ' ').trim();
const out = { active: 'no', answer: 'no', read: '', request: '' };
if (flat === '' || flat.toLowerCase() === 'skip') return out;
// The user's own words, used as written rather than interpreted,
armoured like any other
// text that reaches a prompt, and capped so a pasted essay cannot crowd
the reading out of
// the model's attention. Line breaks are kept: an instruction is often
a small list.
const instruction = harden(raw.replace(/\r/g, '').trim().slice(0,
1200));
out.active = 'yes';
out.answer = 'yes';
out.request = instruction;
out.read = [
'[INSTRUCTIONS · HIGHEST PRIORITY]',
'Everything above still holds: the same completeness, the same detail, the same record format.',
'On top of it, and ahead of it, the person this record is being written for has asked for the following. Where it asks for more than the format above, go further. Where it contradicts the format above, IT WINS - if it says to leave something out, leave it out; if it says to record something the format does not mention, record it anyway.',
'',
instruction,
''
].join('\n');
return out;
param: focus
timeout: 30000
onFailure: ''
silent: true
- label: ARM THE INSTRUCTION ANSWER
type: calc
func: set
param: pendingAnswer
value: '{{focus.answer}}'
format: ''
- condition: '{{focus.active}} = yes'
label: NOTE READING INSTRUCTIONS
type: say
message: >-
🎯 Your instructions are steering the reading and will be carried out on
the finished record.
- label: READ SCREENSHOT BATCHES
type: group
steps:
- label: ANNOUNCE BATCH READING
type: say
message: 👁️ Reading {{batches.length}} segment(s)…
- label: START SEGMENT COUNTER
type: calc
func: set
param: segment
value: '0'
format: ''
- label: BATCH READING LOOP
type: loop
list: batches
steps:
- label: MATERIALIZE BATCH
type: calc
func: set
param: batchImages
value: '{{item}}'
format: ''
- label: COUNT SEGMENT
type: calc
func: increment
param: segment
delta: '1'
- label: COMPUTE SEGMENT RANGE
type: js
args: segment, batchSize, shotCount
code: >-
// FULL PAGE VISION · SEGMENT RANGE
// Which frames a segment is made of. The record marker carries this
range, so a citation
// that says "segment 3" also says "frames 21 to 30" - and the
repair step knows exactly
// where on the page it has to go back to when a segment could not
be read.
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) ||
10));
const seg = Math.max(1, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 1);
const total = Math.max(0, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 0);
const from = (seg - 1) * SIZE + 1;
const to = total > 0 ? Math.min(total, seg * SIZE) : seg * SIZE;
const count = Math.max(0, to - from + 1);
return { from: from, to: to, count: count, size: SIZE, label:
'frames ' + from + '-' + to };
param: range
timeout: 30000
onFailure: ''
silent: true
- label: RESET REPAIR SERIES
type: calc
func: set
param: repairImages
value: ''
format: ''
- label: RESET REPAIR PLAN
type: calc
func: set
param: repairPlan
value: ''
format: ''
- label: RESET REPAIR COUNT FOR SEGMENT
type: calc
func: set
param: repairCount
value: ''
format: ''
- label: CLEAR SEGMENT SLOT
type: calc
func: set
param: segmentNotes
value: ''
format: ''
- label: CLEAR RAW READING
type: calc
func: set
param: rawSegment
value: ''
format: ''
- label: READ SEGMENT
type: gpt
prompt: >-
Please ignore all previous instructions. Write only in
{{outputLanguage}}.
Attached are {{range.count}} screenshots of one web page, in order,
top to bottom. They are frames {{range.from}} to {{range.to}} of
{{shotCount}}. Consecutive frames are exact one-screen steps and do
**not** overlap, so they fit together edge to edge like the pages of
a book.
You are the only reader of these pixels. Turn them into a complete,
faithful record.
[OUTPUT SKELETON - follow it exactly, it is not a suggestion]
Write one block per screenshot, in order. Each block begins with a
heading of exactly this shape:
#### Frame N
N is the absolute frame number. The first attached image is Frame
{{range.from}}, the second is Frame {{range.from}} plus one, and the
last is Frame {{range.to}}. Never restart the count at 1. Never
write Panel, Image, Screenshot, Slide, Page or Figure in place of
Frame. Produce exactly {{range.count}} such headings, one per
attached image, even if an image is blank.
Under each heading write bullet lines only. Every bullet starts with
one of these eight labels and nothing else:
- **Heading:** a heading or title as it appears on screen
- **Text:** any writing - body copy, list items, captions, labels,
footnotes, speech bubbles, narration boxes, sound effects, system
messages. Name the kind in the line when it matters, for example:
speech bubble - "TAKE MY HEART."
- **Table:** followed by a markdown table on the next lines
- **Data:** numbers, prices, dates, scores, ratings, measurements,
chart values, with their units
- **Visual:** a photograph, illustration, icon, logo, avatar, chart,
diagram, map or screenshot, and what it depicts and conveys
- **UI:** buttons, links, tabs, menus, form fields, navigation,
pagination
- **Cut:** an element that is sliced by the frame edge and continues
in the next frame
- **Gap:** [unreadable] - anything cut off, blurred or illegible
[RULES]
- Detail is the entire point of this record. It is the only thing
anyone will have afterwards; the screenshots are discarded once you
are done. Wherever you can choose between a short description and a
thorough one, write the thorough one.
- Describe every picture the way you would for someone who cannot
see it: what it shows, how it is composed, its colours and its
style, any writing inside it, and what it is there to communicate.
An illustration, a photograph, an avatar, an icon that carries
meaning, a chart, a diagram, a map, a screenshot - each one gets its
own line and its own description, never a label like "an image".
- Record the visual form alongside the content wherever the form
carries meaning: how large a heading is against the body text around
it, which typeface it appears to be set in, the colours of text,
backgrounds, borders and buttons, how much space separates the
blocks, and how the blocks are arranged across the screen.
- Transcribe all visible text verbatim. Keep numbers, units, dates
and proper nouns exactly as shown: never round, never normalise,
never translate them.
- Read charts off the pixels: name the axes, the series and every
value you can see.
- Keep the reading order and the visual hierarchy of each frame.
- The frames do not overlap, so nothing in them is a duplicate. A
sentence, a table row or an image that is cut off at the bottom edge
of one frame continues at the top of the next: note it once with
**Cut:** and write the joined, complete version in the frame where
it finishes.
- Bars pinned to the screen - navigation, cookie banners, sticky
footers, chat bubbles, a series or chapter title reprinted above
every screen - do come back on every frame. Record each of them
once, in the first frame it appears in, and ignore it afterwards.
- Never invent anything you cannot see.
- Do not write a document title, a source line, a preamble, a
summary or a closing remark. The record already has a header. Start
with `#### Frame {{range.from}}` and end with the last bullet of
`#### Frame {{range.to}}`.
{{focus.read}}
[SCREENSHOTS]:
{{batchImages}}
[RECORD]:
param: rawSegment
isolated: true
silent: true
- label: SHAPE THE READING
type: js
args: rawSegment, segment, batchSize, shotCount
code: >-
// FULL PAGE VISION · SHAPE SEGMENT
// The record has to look the same in every segment, because
everything downstream reads
// it: the citations, the rebuild, the data extraction, the export.
Left to itself a model
// invents a new layout per call - one segment comes back as "###
[Panel 1]", the next as a
// flat bullet list with no headings at all, the next as "[FRAME 1]"
- and every one of them
// starts counting at one again, so "frame 3" means three different
places in one record.
//
// The prompt prescribes the shape. This step enforces it,
deterministically and for free:
// * every heading variant a model reaches for is recognised and
rewritten to `#### Frame N`
// * N is the ABSOLUTE frame number, so frame 47 is called frame
47 in every segment
// * the bullet labels are folded into one closed vocabulary, with
the original wording
// preserved inside the line so nothing is lost
// * a document title or source line that the model reprinted per
segment is dropped,
// because the record already carries one at the top
//
// THIS STEP MAY NEVER REFUSE A READING.
//
// It used to have a strict mode that returned nothing when the
model had not produced exactly
// one block per screenshot, on the theory that one more attempt
would come back tidier. That
// was a serious mistake: the shape of the record is cosmetic, the
reading is what costs money,
// and wiring the cheap thing to the expensive one turned a
formatting nicety into a gate on
// the whole ladder. A model that writes twenty panels instead of
ten frames is not a failed
// reading - it is a reading that needs renumbering, which happens
here for free. With a forty
// percent shape-deviation rate measured on real output, thirty
segments became sixty-six model
// calls instead of thirty, every one of them silent, plus a fresh
ten-frame capture each time.
// From the outside that is indistinguishable from a hang.
//
// So: whatever comes in, something usable comes out. Too many
blocks are mapped onto the
// frames proportionally, too few are numbered from the start of the
segment and the shortfall
// is stated, none at all are filed under the segment's frame span.
The only empty answer is
// for empty input, which is what the reading ladder above actually
needs to know - and the
// whole body is wrapped so that even an unforeseen error hands the
raw reading back intact
// rather than costing a request.
function harden(value) {
// Text lifted off a page can contain the engine's own {{parameter}} syntax. Left as it is,
// it would be executed the next time this text is interpolated - fetching pages, running
// searches - once per occurrence and once per answer. The documented escape makes the
// braces literal without changing how the text reads. Idempotent by design.
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) ||
10));
const seg = Math.max(1, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 1);
const totalFrames = Math.max(0, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 0);
const from = (seg - 1) * SIZE + 1;
const to = totalFrames > 0 ? Math.min(totalFrames, seg * SIZE) : seg
* SIZE;
const want = Math.max(1, to - from + 1);
const original = String(args.rawSegment == null ? '' :
args.rawSegment).replace(/\r/g, '').trim();
if (original === '') return '';
let raw = original;
try {
// A model that is told "do not write a preamble" still sometimes
writes one. Fenced output,
// a repeated document title, a source line and a horizontal rule at
the very top are all
// duplicates of information the record already holds once.
raw = raw.replace(/^```[a-z]*\s*\n/i, '').replace(/\n```\s*$/i, '');
const lead = raw.split('\n');
while (lead.length > 0) {
const line = lead[0].trim();
const drop =
line === '' ||
line === '---' ||
/^#{1,3}\s+\S/.test(line) && lead.length > 3 && !/frame|panel|image|screenshot|slide|shot/i.test(line) ||
/^[-*]?\s*\**\s*(source|url|page|title|link)\s*\**\s*:/i.test(line) ||
/^(here (is|are)|below (is|are)|sure[,!.]|certainly[,!.]|i('| wi)ll |this (is|segment))/i.test(line);
if (!drop) break;
lead.shift();
}
raw = lead.join('\n').trim();
if (raw === '') return '';
// Canonical bullet labels. The original wording is kept inside the
line, so a speech bubble
// is still recognisable as a speech bubble - it is simply filed
under Text like every other
// piece of transcribed writing.
const LABELS = [
['Heading', /^(heading|title|header|section title|chapter title)$/i, false],
['Table', /^(table|grid|matrix|comparison)$/i, false],
['Data', /^(data|numbers?|values?|stats?|statistics|metrics?|price|prices|pricing|score|scores|rank(ing)?s?)$/i, false],
['UI', /^(ui|ui element|navigation|navigation ui|nav|nav bar|navbar|menu|button|buttons|link|links|form|form field|input|control|controls|tab|tabs|toolbar|footer|footer nav|breadcrumb|pagination)$/i, false],
['Visual', /^(visual|illustration|image|picture|photo|photograph|art|artwork|drawing|panel art|graphic|icon|logo|avatar|chart|diagram|figure|screenshot|map)$/i, true],
['Text', /^(text|speech bubble|speech|dialogue|dialog|narration|narration box|narrative box|text box|textbox|caption|body|body text|paragraph|quote|sound effect|sfx|system text|system window|system message|label|note|footnote|banner|banner box|top text box|bottom banner box|notification)$/i, true],
['Cut', /^(cut|continues|continued|continuation|bridged|carry over|spans)$/i, false],
['Gap', /^(gap|unreadable|illegible|blank|empty|missing|cut off)$/i, false]
];
function normaliseLine(line) {
const m = line.match(/^(\s*)([-*+]|\d+[.)])\s+\**\s*([A-Za-z][A-Za-z /_-]{1,40}?)\s*\**\s*:\s*\**\s*(.*)$/);
if (!m) return line;
const indent = m[1];
const bullet = /^\d/.test(m[2]) ? '-' : '-';
const rawLabel = m[3].trim();
let rest = m[4];
for (let i = 0; i < LABELS.length; i++) {
if (!LABELS[i][1].test(rawLabel)) continue;
const canonical = LABELS[i][0];
const keepOriginal = LABELS[i][2] && rawLabel.toLowerCase() !== canonical.toLowerCase();
const prefix = keepOriginal ? rawLabel.toLowerCase() + ' — ' : '';
return indent + bullet + ' **' + canonical + ':** ' + prefix + rest.trim();
}
return indent + bullet + ' **' + rawLabel + ':** ' + rest.trim();
}
// Every heading shape a model reaches for, in one expression.
const HEAD =
/^[\s>*_]*(?:#{1,6})?[\s*_]*\[?\s*(?:frame|panel|image|screenshot|screen|slide|page|shot|fig(?:ure)?)\s*[#:\-]?\s*(\d+)\s*\]?\s*[:.\-]?[\s*_]*$/i;
const lines = raw.split('\n');
const blocks = [];
let current = null;
let preamble = [];
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(HEAD);
if (m) {
current = { body: [] };
blocks.push(current);
continue;
}
if (current === null) preamble.push(lines[i]);
else current.body.push(lines[i]);
}
if (blocks.length === 0) {
// No frame boundaries at all. The content is intact, only the per-frame granularity is
// gone, so it is filed under the segment's frame span and marked as such rather than
// being silently renumbered into something that would be wrong.
const body = raw.split('\n').map(function (l) { return normaliseLine(l); }).join('\n');
return harden('#### Frames ' + from + '-' + to + '\n' +
'- **Gap:** the reading did not separate these ' + want + ' frames; the content below covers all of them together.\n' +
body.trim() + '\n');
}
if (preamble.join('').trim() !== '') {
blocks[0].body = preamble.concat(blocks[0].body);
}
// Mapping blocks onto frames. Equal counts is the normal case and
maps one to one. More
// blocks than images means the model cut the screenshots into
panels: they are distributed
// proportionally, so panels eleven and twelve of twenty land on
frame six rather than all
// surplus being dumped on the last one. Fewer blocks means it
merged some: they are numbered
// from the start of the segment and the shortfall is stated instead
of being papered over.
const out = [];
let lastNumber = -1;
for (let i = 0; i < blocks.length; i++) {
const number = blocks.length > want
? Math.min(to, from + Math.floor(i * want / blocks.length))
: Math.min(to, from + i);
const body = blocks[i].body
.map(function (l) { return normaliseLine(l); })
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
if (number === lastNumber && out.length > 0) {
if (body) out[out.length - 1] += '\n' + body;
continue;
}
lastNumber = number;
out.push('#### Frame ' + number + '\n' + (body || '- **Gap:** nothing legible in this frame.'));
}
if (blocks.length < want) {
out.push('- **Gap:** the reading separated ' + blocks.length + ' of the ' + want + ' frames in this segment; the rest were merged into the blocks above.');
}
return harden(out.join('\n\n').replace(/\n{3,}/g, '\n\n').trim() +
'\n');
} catch (e) {
// Shaping is a convenience. If anything in it goes wrong the reading still has to reach the
// record, hardened, rather than being reported as a failure that costs another request.
return harden('#### Frames ' + from + '-' + to + '\n' +
'- **Gap:** the reading could not be shaped (' + String((e && e.message) || e) +
'); it is filed here unchanged.\n' + original);
}
param: segmentNotes
timeout: 30000
onFailure: ''
silent: true
- condition: '{{segmentNotes}} ='
label: CLEAR RAW BEFORE RETRY
type: calc
func: set
param: rawSegment
value: ''
format: ''
- condition: '{{segmentNotes}} ='
label: RETRY SEGMENT
type: gpt
prompt: >-
Please ignore all previous instructions. Write only in
{{outputLanguage}}.
Attached are {{range.count}} screenshots of one web page, in order,
top to bottom. They are frames {{range.from}} to {{range.to}} of
{{shotCount}}. Consecutive frames are exact one-screen steps and do
**not** overlap, so they fit together edge to edge like the pages of
a book.
You are the only reader of these pixels. Turn them into a complete,
faithful record.
[OUTPUT SKELETON - follow it exactly, it is not a suggestion]
Write one block per screenshot, in order. Each block begins with a
heading of exactly this shape:
#### Frame N
N is the absolute frame number. The first attached image is Frame
{{range.from}}, the second is Frame {{range.from}} plus one, and the
last is Frame {{range.to}}. Never restart the count at 1. Never
write Panel, Image, Screenshot, Slide, Page or Figure in place of
Frame. Produce exactly {{range.count}} such headings, one per
attached image, even if an image is blank.
Under each heading write bullet lines only. Every bullet starts with
one of these eight labels and nothing else:
- **Heading:** a heading or title as it appears on screen
- **Text:** any writing - body copy, list items, captions, labels,
footnotes, speech bubbles, narration boxes, sound effects, system
messages. Name the kind in the line when it matters, for example:
speech bubble - "TAKE MY HEART."
- **Table:** followed by a markdown table on the next lines
- **Data:** numbers, prices, dates, scores, ratings, measurements,
chart values, with their units
- **Visual:** a photograph, illustration, icon, logo, avatar, chart,
diagram, map or screenshot, and what it depicts and conveys
- **UI:** buttons, links, tabs, menus, form fields, navigation,
pagination
- **Cut:** an element that is sliced by the frame edge and continues
in the next frame
- **Gap:** [unreadable] - anything cut off, blurred or illegible
[RULES]
- Detail is the entire point of this record. It is the only thing
anyone will have afterwards; the screenshots are discarded once you
are done. Wherever you can choose between a short description and a
thorough one, write the thorough one.
- Describe every picture the way you would for someone who cannot
see it: what it shows, how it is composed, its colours and its
style, any writing inside it, and what it is there to communicate.
An illustration, a photograph, an avatar, an icon that carries
meaning, a chart, a diagram, a map, a screenshot - each one gets its
own line and its own description, never a label like "an image".
- Record the visual form alongside the content wherever the form
carries meaning: how large a heading is against the body text around
it, which typeface it appears to be set in, the colours of text,
backgrounds, borders and buttons, how much space separates the
blocks, and how the blocks are arranged across the screen.
- Transcribe all visible text verbatim. Keep numbers, units, dates
and proper nouns exactly as shown: never round, never normalise,
never translate them.
- Read charts off the pixels: name the axes, the series and every
value you can see.
- Keep the reading order and the visual hierarchy of each frame.
- The frames do not overlap, so nothing in them is a duplicate. A
sentence, a table row or an image that is cut off at the bottom edge
of one frame continues at the top of the next: note it once with
**Cut:** and write the joined, complete version in the frame where
it finishes.
- Bars pinned to the screen - navigation, cookie banners, sticky
footers, chat bubbles, a series or chapter title reprinted above
every screen - do come back on every frame. Record each of them
once, in the first frame it appears in, and ignore it afterwards.
- Never invent anything you cannot see.
- Do not write a document title, a source line, a preamble, a
summary or a closing remark. The record already has a header. Start
with `#### Frame {{range.from}}` and end with the last bullet of
`#### Frame {{range.to}}`.
{{focus.read}}
[SCREENSHOTS]:
{{batchImages}}
[RECORD]:
param: rawSegment
isolated: true
silent: true
- condition: '{{segmentNotes}} ='
label: SHAPE THE RETRY
type: js
args: rawSegment, segment, batchSize, shotCount
code: >-
// FULL PAGE VISION · SHAPE SEGMENT
// The record has to look the same in every segment, because
everything downstream reads
// it: the citations, the rebuild, the data extraction, the export.
Left to itself a model
// invents a new layout per call - one segment comes back as "###
[Panel 1]", the next as a
// flat bullet list with no headings at all, the next as "[FRAME 1]"
- and every one of them
// starts counting at one again, so "frame 3" means three different
places in one record.
//
// The prompt prescribes the shape. This step enforces it,
deterministically and for free:
// * every heading variant a model reaches for is recognised and
rewritten to `#### Frame N`
// * N is the ABSOLUTE frame number, so frame 47 is called frame
47 in every segment
// * the bullet labels are folded into one closed vocabulary, with
the original wording
// preserved inside the line so nothing is lost
// * a document title or source line that the model reprinted per
segment is dropped,
// because the record already carries one at the top
//
// THIS STEP MAY NEVER REFUSE A READING.
//
// It used to have a strict mode that returned nothing when the
model had not produced exactly
// one block per screenshot, on the theory that one more attempt
would come back tidier. That
// was a serious mistake: the shape of the record is cosmetic, the
reading is what costs money,
// and wiring the cheap thing to the expensive one turned a
formatting nicety into a gate on
// the whole ladder. A model that writes twenty panels instead of
ten frames is not a failed
// reading - it is a reading that needs renumbering, which happens
here for free. With a forty
// percent shape-deviation rate measured on real output, thirty
segments became sixty-six model
// calls instead of thirty, every one of them silent, plus a fresh
ten-frame capture each time.
// From the outside that is indistinguishable from a hang.
//
// So: whatever comes in, something usable comes out. Too many
blocks are mapped onto the
// frames proportionally, too few are numbered from the start of the
segment and the shortfall
// is stated, none at all are filed under the segment's frame span.
The only empty answer is
// for empty input, which is what the reading ladder above actually
needs to know - and the
// whole body is wrapped so that even an unforeseen error hands the
raw reading back intact
// rather than costing a request.
function harden(value) {
// Text lifted off a page can contain the engine's own {{parameter}} syntax. Left as it is,
// it would be executed the next time this text is interpolated - fetching pages, running
// searches - once per occurrence and once per answer. The documented escape makes the
// braces literal without changing how the text reads. Idempotent by design.
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) ||
10));
const seg = Math.max(1, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 1);
const totalFrames = Math.max(0, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 0);
const from = (seg - 1) * SIZE + 1;
const to = totalFrames > 0 ? Math.min(totalFrames, seg * SIZE) : seg
* SIZE;
const want = Math.max(1, to - from + 1);
const original = String(args.rawSegment == null ? '' :
args.rawSegment).replace(/\r/g, '').trim();
if (original === '') return '';
let raw = original;
try {
// A model that is told "do not write a preamble" still sometimes
writes one. Fenced output,
// a repeated document title, a source line and a horizontal rule at
the very top are all
// duplicates of information the record already holds once.
raw = raw.replace(/^```[a-z]*\s*\n/i, '').replace(/\n```\s*$/i, '');
const lead = raw.split('\n');
while (lead.length > 0) {
const line = lead[0].trim();
const drop =
line === '' ||
line === '---' ||
/^#{1,3}\s+\S/.test(line) && lead.length > 3 && !/frame|panel|image|screenshot|slide|shot/i.test(line) ||
/^[-*]?\s*\**\s*(source|url|page|title|link)\s*\**\s*:/i.test(line) ||
/^(here (is|are)|below (is|are)|sure[,!.]|certainly[,!.]|i('| wi)ll |this (is|segment))/i.test(line);
if (!drop) break;
lead.shift();
}
raw = lead.join('\n').trim();
if (raw === '') return '';
// Canonical bullet labels. The original wording is kept inside the
line, so a speech bubble
// is still recognisable as a speech bubble - it is simply filed
under Text like every other
// piece of transcribed writing.
const LABELS = [
['Heading', /^(heading|title|header|section title|chapter title)$/i, false],
['Table', /^(table|grid|matrix|comparison)$/i, false],
['Data', /^(data|numbers?|values?|stats?|statistics|metrics?|price|prices|pricing|score|scores|rank(ing)?s?)$/i, false],
['UI', /^(ui|ui element|navigation|navigation ui|nav|nav bar|navbar|menu|button|buttons|link|links|form|form field|input|control|controls|tab|tabs|toolbar|footer|footer nav|breadcrumb|pagination)$/i, false],
['Visual', /^(visual|illustration|image|picture|photo|photograph|art|artwork|drawing|panel art|graphic|icon|logo|avatar|chart|diagram|figure|screenshot|map)$/i, true],
['Text', /^(text|speech bubble|speech|dialogue|dialog|narration|narration box|narrative box|text box|textbox|caption|body|body text|paragraph|quote|sound effect|sfx|system text|system window|system message|label|note|footnote|banner|banner box|top text box|bottom banner box|notification)$/i, true],
['Cut', /^(cut|continues|continued|continuation|bridged|carry over|spans)$/i, false],
['Gap', /^(gap|unreadable|illegible|blank|empty|missing|cut off)$/i, false]
];
function normaliseLine(line) {
const m = line.match(/^(\s*)([-*+]|\d+[.)])\s+\**\s*([A-Za-z][A-Za-z /_-]{1,40}?)\s*\**\s*:\s*\**\s*(.*)$/);
if (!m) return line;
const indent = m[1];
const bullet = /^\d/.test(m[2]) ? '-' : '-';
const rawLabel = m[3].trim();
let rest = m[4];
for (let i = 0; i < LABELS.length; i++) {
if (!LABELS[i][1].test(rawLabel)) continue;
const canonical = LABELS[i][0];
const keepOriginal = LABELS[i][2] && rawLabel.toLowerCase() !== canonical.toLowerCase();
const prefix = keepOriginal ? rawLabel.toLowerCase() + ' — ' : '';
return indent + bullet + ' **' + canonical + ':** ' + prefix + rest.trim();
}
return indent + bullet + ' **' + rawLabel + ':** ' + rest.trim();
}
// Every heading shape a model reaches for, in one expression.
const HEAD =
/^[\s>*_]*(?:#{1,6})?[\s*_]*\[?\s*(?:frame|panel|image|screenshot|screen|slide|page|shot|fig(?:ure)?)\s*[#:\-]?\s*(\d+)\s*\]?\s*[:.\-]?[\s*_]*$/i;
const lines = raw.split('\n');
const blocks = [];
let current = null;
let preamble = [];
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(HEAD);
if (m) {
current = { body: [] };
blocks.push(current);
continue;
}
if (current === null) preamble.push(lines[i]);
else current.body.push(lines[i]);
}
if (blocks.length === 0) {
// No frame boundaries at all. The content is intact, only the per-frame granularity is
// gone, so it is filed under the segment's frame span and marked as such rather than
// being silently renumbered into something that would be wrong.
const body = raw.split('\n').map(function (l) { return normaliseLine(l); }).join('\n');
return harden('#### Frames ' + from + '-' + to + '\n' +
'- **Gap:** the reading did not separate these ' + want + ' frames; the content below covers all of them together.\n' +
body.trim() + '\n');
}
if (preamble.join('').trim() !== '') {
blocks[0].body = preamble.concat(blocks[0].body);
}
// Mapping blocks onto frames. Equal counts is the normal case and
maps one to one. More
// blocks than images means the model cut the screenshots into
panels: they are distributed
// proportionally, so panels eleven and twelve of twenty land on
frame six rather than all
// surplus being dumped on the last one. Fewer blocks means it
merged some: they are numbered
// from the start of the segment and the shortfall is stated instead
of being papered over.
const out = [];
let lastNumber = -1;
for (let i = 0; i < blocks.length; i++) {
const number = blocks.length > want
? Math.min(to, from + Math.floor(i * want / blocks.length))
: Math.min(to, from + i);
const body = blocks[i].body
.map(function (l) { return normaliseLine(l); })
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
if (number === lastNumber && out.length > 0) {
if (body) out[out.length - 1] += '\n' + body;
continue;
}
lastNumber = number;
out.push('#### Frame ' + number + '\n' + (body || '- **Gap:** nothing legible in this frame.'));
}
if (blocks.length < want) {
out.push('- **Gap:** the reading separated ' + blocks.length + ' of the ' + want + ' frames in this segment; the rest were merged into the blocks above.');
}
return harden(out.join('\n\n').replace(/\n{3,}/g, '\n\n').trim() +
'\n');
} catch (e) {
// Shaping is a convenience. If anything in it goes wrong the reading still has to reach the
// record, hardened, rather than being reported as a failure that costs another request.
return harden('#### Frames ' + from + '-' + to + '\n' +
'- **Gap:** the reading could not be shaped (' + String((e && e.message) || e) +
'); it is filed here unchanged.\n' + original);
}
param: segmentNotes
timeout: 30000
onFailure: ''
silent: true
- label: SET REPAIR MODE OFF
type: calc
func: set
param: repairMode
value: 'no'
format: ''
- condition: '{{segmentNotes}} ='
label: ENABLE SEGMENT REPAIR
type: calc
func: set
param: repairMode
value: '{{sourceKind}}'
format: ''
- condition:
- '{{repairMode}} = html'
- '{{repairMode}} = pdf'
label: ANNOUNCE SEGMENT REPAIR
type: say
message: >-
🔧 Segment {{segment}} ({{range.label}}) came back empty twice.
Photographing those frames again.
- condition:
- '{{repairMode}} = html'
- '{{repairMode}} = pdf'
label: PLAN SEGMENT REPAIR
type: js
args: segment, batchSize, shotCount
code: >-
// FULL PAGE VISION · PLAN SEGMENT REPAIR
// Reached only when a segment came back empty twice. Instead of
leaving a hole in the
// record, the walk goes back to exactly where that segment was
photographed and takes
// the frames again - fresh pixels rather than a fourth attempt on
the same tokens.
//
// The position is arithmetic, not a search: frame k sits at startY
+ (k-1) * stride,
// and the stride is the exact one-screen step the walk used. The
page has not been
// reloaded since, so the lazily loaded content is still there.
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) ||
10));
const seg = Math.max(1, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 1);
const total = Math.max(0, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 0);
const from = (seg - 1) * SIZE + 1;
const to = total > 0 ? Math.min(total, seg * SIZE) : seg * SIZE;
const count = Math.max(0, to - from + 1);
if (count <= 0) return [];
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
function docScroller() {
return document.scrollingElement || document.documentElement || document.body;
}
function setPos(el, y) {
const target = Math.max(0, Math.round(y));
if (el) { el.scrollTop = target; return; }
try { window.scrollTo({ top: target, behavior: 'instant' }); } catch (e) { /* ignore */ }
try { window.scrollTo({ top: target, behavior: 'auto' }); } catch (e) { /* ignore */ }
try { window.scroll(0, target); } catch (e) { /* ignore */ }
const se = docScroller();
if (se) se.scrollTop = target;
if (document.body) document.body.scrollTop = target;
}
const plan = [];
for (let i = 0; i < count; i++) plan.push({ n: i + 1, frame: from +
i, page: from + i });
// On a PDF the frames come from navigation, not from scrolling, so
there is nothing to
// seek here and the plan is handed back as it is.
try {
const S = window.__fpv;
if (S && typeof S === 'object') {
const el = (S.el && document.contains(S.el)) ? S.el : null;
const stride = Math.max(120, Number(S.stride) || 0);
const startY = Number(S.startY) || 0;
setPos(el, startY + (from - 1) * stride);
await sleep(320);
S.repairing = true;
}
} catch (e) { /* the plan still stands */ }
return plan;
param: repairPlan
timeout: 60000
onFailure: ''
silent: true
- condition: '{{repairMode}} = html'
label: REPAIR HTML SEGMENT
type: loop
list: repairPlan
steps:
- label: APPEND REPAIR FRAME
type: js
args: repairImages, view
code: >-
// FULL PAGE VISION · APPEND REPAIR FRAME
// The same accumulation as the main walk, and for the same
reason: a comma separated series
// held in a real parameter is the only form proven to deliver
several images to one prompt.
//
// arrayValue += ',' + viewValue -> a real parameter ->
used in the prompt
//
// Two rules follow from that and must not be touched:
// * the separator is a COMMA. HARPA splits the stored series
on commas to resolve the
// individual attachments. A newline joined series arrives
as a single blob and only one
// image reaches the model.
// * the token travels as a JS ARGUMENT. Interpolating
{{view}} inside a calc step coerces
// the reference to text and breaks it, which surfaces as
// "Failed to execute 'createImageBitmap'".
//
// This is the REPAIR variant and it reads its own slot. It used
to read args['shots'], which
// is not among its arguments: the accumulator was therefore
undefined on every pass, the
// series restarted from empty each time, and a repair that
photographed ten frames handed
// exactly one of them to the reading. The slot name is the
whole fix.
//
// It also deliberately ignores the two guards the main walk
needs. The frame ceiling counts
// the frames of the WALK, and by the time a repair runs the
walk has usually reached it, so
// applying it here would refuse every repair frame on exactly
the runs that need repairing.
// The walk's abort flag means the same thing one step further
on. The repair cannot run away
// regardless: its plan has a fixed length, and a refused frame
costs one entry of that plan
// and nothing more.
const arrayValue = args['repairImages'] || '';
const viewValue = args.view;
if (!viewValue) {
const S = window.__fpv || (window.__fpv = {});
S.repairMisses = (Number(S.repairMisses) || 0) + 1;
throw new Error('repair frame not available yet');
}
if (arrayValue === '') return viewValue;
return arrayValue + ',' + viewValue;
param: repairImages
timeout: 60000
onFailure: PACE REPAIR
silent: true
- label: ADVANCE REPAIR FRAME
type: js
args: ''
code: >-
// FULL PAGE VISION · ADVANCE REPAIR FRAME
// One exact screen forward inside a repair pass. Deliberately
separate from the main
// walk: it must not touch the frame counter, the abort flag or
the lazy-load logic of
// the capture state, because the capture is long finished and
only these few frames are
// being taken again.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
function docScroller() {
return document.scrollingElement || document.documentElement || document.body;
}
function posOf(el) {
if (el) return el.scrollTop || 0;
const se = docScroller();
const w = typeof window.pageYOffset === 'number' ? window.pageYOffset : 0;
return Math.max(w, se ? (se.scrollTop || 0) : 0, document.body ? (document.body.scrollTop || 0) : 0);
}
function setPos(el, y) {
const target = Math.max(0, Math.round(y));
if (el) { el.scrollTop = target; return; }
try { window.scrollTo({ top: target, behavior: 'instant' }); } catch (e) { /* ignore */ }
try { window.scrollTo({ top: target, behavior: 'auto' }); } catch (e) { /* ignore */ }
try { window.scroll(0, target); } catch (e) { /* ignore */ }
const se = docScroller();
if (se) se.scrollTop = target;
if (document.body) document.body.scrollTop = target;
}
try {
const S = window.__fpv || {};
const el = (S.el && document.contains(S.el)) ? S.el : null;
const stride = Math.max(120, Number(S.stride) || (window.innerHeight || 800));
const before = posOf(el);
setPos(el, before + stride);
await sleep(140);
return String(posOf(el));
} catch (e) {
return '0';
}
param: scratch
timeout: 60000
onFailure: ''
silent: true
- label: PACE REPAIR
type: js
args: captureInterval
code: >-
// FULL PAGE VISION · PACE REPAIR
// Chrome refuses more than two screenshots per second and the
limit cannot be raised, so
// a repair pass keeps the same rhythm as the walk. This is also
the landing point when a
// repair frame is refused, which is why it does nothing except
wait: the repair plan has
// a fixed length, so a refused frame simply costs one entry and
can never spin.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
const raw = String(args.captureInterval == null ? '' :
args.captureInterval).replace(/[^0-9]/g, '');
const interval = Math.max(200, Math.min(5000, Number(raw) ||
520));
await sleep(interval);
return String(interval);
param: scratch
timeout: 30000
onFailure: ''
silent: true
- condition: '{{repairMode}} = pdf'
label: REPAIR PDF SEGMENT
type: loop
list: repairPlan
steps:
- label: OPEN REPAIR PAGE
type: navigate
url: '{{pdfUrl}}#page={{item.page}}'
waitForIdle: true
onFailure: ''
silent: true
- label: SETTLE REPAIR PAGE
type: wait
for: custom-delay
delay: 400
silent: true
- label: APPEND REPAIR PDF FRAME
type: js
args: repairImages, view
code: >-
// FULL PAGE VISION · APPEND REPAIR FRAME
// The same accumulation as the main walk, and for the same
reason: a comma separated series
// held in a real parameter is the only form proven to deliver
several images to one prompt.
//
// arrayValue += ',' + viewValue -> a real parameter ->
used in the prompt
//
// Two rules follow from that and must not be touched:
// * the separator is a COMMA. HARPA splits the stored series
on commas to resolve the
// individual attachments. A newline joined series arrives
as a single blob and only one
// image reaches the model.
// * the token travels as a JS ARGUMENT. Interpolating
{{view}} inside a calc step coerces
// the reference to text and breaks it, which surfaces as
// "Failed to execute 'createImageBitmap'".
//
// This is the REPAIR variant and it reads its own slot. It used
to read args['shots'], which
// is not among its arguments: the accumulator was therefore
undefined on every pass, the
// series restarted from empty each time, and a repair that
photographed ten frames handed
// exactly one of them to the reading. The slot name is the
whole fix.
//
// It also deliberately ignores the two guards the main walk
needs. The frame ceiling counts
// the frames of the WALK, and by the time a repair runs the
walk has usually reached it, so
// applying it here would refuse every repair frame on exactly
the runs that need repairing.
// The walk's abort flag means the same thing one step further
on. The repair cannot run away
// regardless: its plan has a fixed length, and a refused frame
costs one entry of that plan
// and nothing more.
const arrayValue = args['repairImages'] || '';
const viewValue = args.view;
if (!viewValue) {
const S = window.__fpv || (window.__fpv = {});
S.repairMisses = (Number(S.repairMisses) || 0) + 1;
throw new Error('repair frame not available yet');
}
if (arrayValue === '') return viewValue;
return arrayValue + ',' + viewValue;
param: repairImages
timeout: 60000
onFailure: PACE PDF REPAIR
silent: true
- label: PACE PDF REPAIR
type: js
args: captureInterval
code: >-
// FULL PAGE VISION · PACE REPAIR
// Chrome refuses more than two screenshots per second and the
limit cannot be raised, so
// a repair pass keeps the same rhythm as the walk. This is also
the landing point when a
// repair frame is refused, which is why it does nothing except
wait: the repair plan has
// a fixed length, so a refused frame simply costs one entry and
can never spin.
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
const raw = String(args.captureInterval == null ? '' :
args.captureInterval).replace(/[^0-9]/g, '');
const interval = Math.max(200, Math.min(5000, Number(raw) ||
520));
await sleep(interval);
return String(interval);
param: scratch
timeout: 30000
onFailure: ''
silent: true
- condition: '{{repairMode}} = html'
label: RESTORE POSITION AFTER REPAIR
type: js
args: ''
code: >-
// FULL PAGE VISION · RESTORE POSITION AFTER REPAIR
// Puts the page back where the reader left it, so a repair pass is
invisible afterwards.
function docScroller() {
return document.scrollingElement || document.documentElement || document.body;
}
try {
const S = window.__fpv || {};
const el = (S.el && document.contains(S.el)) ? S.el : null;
const home = Number(S.startY) || 0;
if (el) {
el.scrollTop = home;
} else {
try { window.scrollTo({ top: home, behavior: 'instant' }); } catch (e) { /* ignore */ }
try { window.scrollTo({ top: home, behavior: 'auto' }); } catch (e) { /* ignore */ }
try { window.scroll(0, home); } catch (e) { /* ignore */ }
const se = docScroller();
if (se) se.scrollTop = home;
if (document.body) document.body.scrollTop = home;
}
S.repairing = false;
return 'restored';
} catch (e) {
return 'skipped';
}
param: scratch
timeout: 30000
onFailure: ''
silent: true
- condition: '{{repairMode}} = pdf'
label: RETURN AFTER PDF REPAIR
type: navigate
url: '{{pdfUrl}}'
waitForIdle: false
onFailure: ''
silent: true
- condition:
- '{{repairMode}} = html'
- '{{repairMode}} = pdf'
label: COUNT REPAIR FRAMES
type: js
args: repairImages
code: >-
// FULL PAGE VISION · COUNT A FRAME SERIES
// Answers "did the repair capture actually produce frames" with a
NUMBER.
//
// The obvious way to ask that is a condition on the series itself.
That must never happen: a
// frame token is a reference, not a string, and interpolating the
series into a condition or
// a calc value coerces it to text and destroys it - the failure
surfaces later as
// "Failed to execute 'createImageBitmap'". The series therefore
only ever travels as a JS
// argument, and only this count comes back out.
const raw = String(args.repairImages == null ? '' :
args.repairImages);
const parts = raw.split(',').map(function (s) { return s.trim();
}).filter(function (s) { return s.length > 0; });
return { parts: parts.length, ok: parts.length > 0 ? 'yes' : 'no' };
param: repairCount
timeout: 30000
onFailure: ''
silent: true
- condition: '{{repairCount.ok}} = yes'
label: MATERIALIZE REPAIR
type: calc
func: set
param: batchImages
value: '{{repairImages}}'
format: ''
- condition: '{{segmentNotes}} ='
label: CLEAR RAW BEFORE THIRD READING
type: calc
func: set
param: rawSegment
value: ''
format: ''
- condition: '{{segmentNotes}} ='
label: READ SEGMENT AGAIN
type: gpt
prompt: >-
Please ignore all previous instructions. Write only in
{{outputLanguage}}.
Attached are {{range.count}} screenshots of one web page, in order,
top to bottom. They are frames {{range.from}} to {{range.to}} of
{{shotCount}}. They were photographed again for this attempt, so
these are fresh pixels.
Two earlier attempts at these images came back empty. Do not aim for
a polished record this time — aim for something rather than nothing.
Write what you can see. One `#### Frame N` heading per image,
starting at Frame {{range.from}} and counting upwards, then plain
bullet lines underneath saying what stands there: text as text,
transcribed as exactly as you can manage, and a description of
anything shown as a picture, a chart or a control - what it depicts,
not just that it exists.
If an image is genuinely blank or illegible, write one bullet saying
so and move to the next one. Do not explain, do not apologise, do
not describe what you are about to do — just write the frames.
{{focus.read}}
[SCREENSHOTS]:
{{batchImages}}
[RECORD]:
param: rawSegment
isolated: true
silent: true
- condition: '{{segmentNotes}} ='
label: SHAPE THE THIRD READING
type: js
args: rawSegment, segment, batchSize, shotCount
code: >-
// FULL PAGE VISION · SHAPE SEGMENT
// The record has to look the same in every segment, because
everything downstream reads
// it: the citations, the rebuild, the data extraction, the export.
Left to itself a model
// invents a new layout per call - one segment comes back as "###
[Panel 1]", the next as a
// flat bullet list with no headings at all, the next as "[FRAME 1]"
- and every one of them
// starts counting at one again, so "frame 3" means three different
places in one record.
//
// The prompt prescribes the shape. This step enforces it,
deterministically and for free:
// * every heading variant a model reaches for is recognised and
rewritten to `#### Frame N`
// * N is the ABSOLUTE frame number, so frame 47 is called frame
47 in every segment
// * the bullet labels are folded into one closed vocabulary, with
the original wording
// preserved inside the line so nothing is lost
// * a document title or source line that the model reprinted per
segment is dropped,
// because the record already carries one at the top
//
// THIS STEP MAY NEVER REFUSE A READING.
//
// It used to have a strict mode that returned nothing when the
model had not produced exactly
// one block per screenshot, on the theory that one more attempt
would come back tidier. That
// was a serious mistake: the shape of the record is cosmetic, the
reading is what costs money,
// and wiring the cheap thing to the expensive one turned a
formatting nicety into a gate on
// the whole ladder. A model that writes twenty panels instead of
ten frames is not a failed
// reading - it is a reading that needs renumbering, which happens
here for free. With a forty
// percent shape-deviation rate measured on real output, thirty
segments became sixty-six model
// calls instead of thirty, every one of them silent, plus a fresh
ten-frame capture each time.
// From the outside that is indistinguishable from a hang.
//
// So: whatever comes in, something usable comes out. Too many
blocks are mapped onto the
// frames proportionally, too few are numbered from the start of the
segment and the shortfall
// is stated, none at all are filed under the segment's frame span.
The only empty answer is
// for empty input, which is what the reading ladder above actually
needs to know - and the
// whole body is wrapped so that even an unforeseen error hands the
raw reading back intact
// rather than costing a request.
function harden(value) {
// Text lifted off a page can contain the engine's own {{parameter}} syntax. Left as it is,
// it would be executed the next time this text is interpolated - fetching pages, running
// searches - once per occurrence and once per answer. The documented escape makes the
// braces literal without changing how the text reads. Idempotent by design.
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) ||
10));
const seg = Math.max(1, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 1);
const totalFrames = Math.max(0, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 0);
const from = (seg - 1) * SIZE + 1;
const to = totalFrames > 0 ? Math.min(totalFrames, seg * SIZE) : seg
* SIZE;
const want = Math.max(1, to - from + 1);
const original = String(args.rawSegment == null ? '' :
args.rawSegment).replace(/\r/g, '').trim();
if (original === '') return '';
let raw = original;
try {
// A model that is told "do not write a preamble" still sometimes
writes one. Fenced output,
// a repeated document title, a source line and a horizontal rule at
the very top are all
// duplicates of information the record already holds once.
raw = raw.replace(/^```[a-z]*\s*\n/i, '').replace(/\n```\s*$/i, '');
const lead = raw.split('\n');
while (lead.length > 0) {
const line = lead[0].trim();
const drop =
line === '' ||
line === '---' ||
/^#{1,3}\s+\S/.test(line) && lead.length > 3 && !/frame|panel|image|screenshot|slide|shot/i.test(line) ||
/^[-*]?\s*\**\s*(source|url|page|title|link)\s*\**\s*:/i.test(line) ||
/^(here (is|are)|below (is|are)|sure[,!.]|certainly[,!.]|i('| wi)ll |this (is|segment))/i.test(line);
if (!drop) break;
lead.shift();
}
raw = lead.join('\n').trim();
if (raw === '') return '';
// Canonical bullet labels. The original wording is kept inside the
line, so a speech bubble
// is still recognisable as a speech bubble - it is simply filed
under Text like every other
// piece of transcribed writing.
const LABELS = [
['Heading', /^(heading|title|header|section title|chapter title)$/i, false],
['Table', /^(table|grid|matrix|comparison)$/i, false],
['Data', /^(data|numbers?|values?|stats?|statistics|metrics?|price|prices|pricing|score|scores|rank(ing)?s?)$/i, false],
['UI', /^(ui|ui element|navigation|navigation ui|nav|nav bar|navbar|menu|button|buttons|link|links|form|form field|input|control|controls|tab|tabs|toolbar|footer|footer nav|breadcrumb|pagination)$/i, false],
['Visual', /^(visual|illustration|image|picture|photo|photograph|art|artwork|drawing|panel art|graphic|icon|logo|avatar|chart|diagram|figure|screenshot|map)$/i, true],
['Text', /^(text|speech bubble|speech|dialogue|dialog|narration|narration box|narrative box|text box|textbox|caption|body|body text|paragraph|quote|sound effect|sfx|system text|system window|system message|label|note|footnote|banner|banner box|top text box|bottom banner box|notification)$/i, true],
['Cut', /^(cut|continues|continued|continuation|bridged|carry over|spans)$/i, false],
['Gap', /^(gap|unreadable|illegible|blank|empty|missing|cut off)$/i, false]
];
function normaliseLine(line) {
const m = line.match(/^(\s*)([-*+]|\d+[.)])\s+\**\s*([A-Za-z][A-Za-z /_-]{1,40}?)\s*\**\s*:\s*\**\s*(.*)$/);
if (!m) return line;
const indent = m[1];
const bullet = /^\d/.test(m[2]) ? '-' : '-';
const rawLabel = m[3].trim();
let rest = m[4];
for (let i = 0; i < LABELS.length; i++) {
if (!LABELS[i][1].test(rawLabel)) continue;
const canonical = LABELS[i][0];
const keepOriginal = LABELS[i][2] && rawLabel.toLowerCase() !== canonical.toLowerCase();
const prefix = keepOriginal ? rawLabel.toLowerCase() + ' — ' : '';
return indent + bullet + ' **' + canonical + ':** ' + prefix + rest.trim();
}
return indent + bullet + ' **' + rawLabel + ':** ' + rest.trim();
}
// Every heading shape a model reaches for, in one expression.
const HEAD =
/^[\s>*_]*(?:#{1,6})?[\s*_]*\[?\s*(?:frame|panel|image|screenshot|screen|slide|page|shot|fig(?:ure)?)\s*[#:\-]?\s*(\d+)\s*\]?\s*[:.\-]?[\s*_]*$/i;
const lines = raw.split('\n');
const blocks = [];
let current = null;
let preamble = [];
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(HEAD);
if (m) {
current = { body: [] };
blocks.push(current);
continue;
}
if (current === null) preamble.push(lines[i]);
else current.body.push(lines[i]);
}
if (blocks.length === 0) {
// No frame boundaries at all. The content is intact, only the per-frame granularity is
// gone, so it is filed under the segment's frame span and marked as such rather than
// being silently renumbered into something that would be wrong.
const body = raw.split('\n').map(function (l) { return normaliseLine(l); }).join('\n');
return harden('#### Frames ' + from + '-' + to + '\n' +
'- **Gap:** the reading did not separate these ' + want + ' frames; the content below covers all of them together.\n' +
body.trim() + '\n');
}
if (preamble.join('').trim() !== '') {
blocks[0].body = preamble.concat(blocks[0].body);
}
// Mapping blocks onto frames. Equal counts is the normal case and
maps one to one. More
// blocks than images means the model cut the screenshots into
panels: they are distributed
// proportionally, so panels eleven and twelve of twenty land on
frame six rather than all
// surplus being dumped on the last one. Fewer blocks means it
merged some: they are numbered
// from the start of the segment and the shortfall is stated instead
of being papered over.
const out = [];
let lastNumber = -1;
for (let i = 0; i < blocks.length; i++) {
const number = blocks.length > want
? Math.min(to, from + Math.floor(i * want / blocks.length))
: Math.min(to, from + i);
const body = blocks[i].body
.map(function (l) { return normaliseLine(l); })
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
if (number === lastNumber && out.length > 0) {
if (body) out[out.length - 1] += '\n' + body;
continue;
}
lastNumber = number;
out.push('#### Frame ' + number + '\n' + (body || '- **Gap:** nothing legible in this frame.'));
}
if (blocks.length < want) {
out.push('- **Gap:** the reading separated ' + blocks.length + ' of the ' + want + ' frames in this segment; the rest were merged into the blocks above.');
}
return harden(out.join('\n\n').replace(/\n{3,}/g, '\n\n').trim() +
'\n');
} catch (e) {
// Shaping is a convenience. If anything in it goes wrong the reading still has to reach the
// record, hardened, rather than being reported as a failure that costs another request.
return harden('#### Frames ' + from + '-' + to + '\n' +
'- **Gap:** the reading could not be shaped (' + String((e && e.message) || e) +
'); it is filed here unchanged.\n' + original);
}
param: segmentNotes
timeout: 30000
onFailure: ''
silent: true
- condition: '{{segmentNotes}} ='
label: COUNT FAILED SEGMENT
type: calc
func: increment
param: failedSegments
delta: '1'
- condition: '{{segmentNotes}} ='
label: MARK SEGMENT GAP
type: calc
func: set
param: segmentNotes
value: >-
- **Gap:** [SEGMENT {{segment}} NOT READ - {{range.label}}. Three
readings came back empty, one of them on freshly taken screenshots.
These frames are missing from the record.]
format: ''
- label: RENDER SEGMENT PROGRESS
type: js
args: segment, batchSize, shotCount, segmentNotes
code: >-
// FULL PAGE VISION · SEGMENT PROGRESS
// One line per finished segment. It exists because reading a long
page is a minute of
// silence otherwise: the readings themselves no longer go into the
chat, so without this
// there is nothing between "reading 7 segments" and the finished
record. The bar is a
// fixed ten cells wide whatever the segment count is, so the lines
stay aligned under
// each other and the eye only has to follow one moving edge.
//
// It also carries the honest status of each segment: a segment that
had to be salvaged or
// could not be read at all says so here, at the moment it happens,
instead of only turning
// up as a warning at the end.
const CELLS = 10;
// Everything is derived from the three numbers the loop already
carries, so the line needs
// no extra parameters set up around it.
const SIZE = Math.max(1, Math.min(10, Number(args.batchSize) ||
10));
const shots = Math.max(1, Number(String(args.shotCount ||
'').replace(/[^0-9]/g, '')) || 1);
const done = Math.max(0, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 0);
const total = Math.max(1, Math.ceil(shots / SIZE));
const from = (done - 1) * SIZE + 1;
const to = Math.min(shots, done * SIZE);
const label = 'frames ' + from + '-' + to;
const body = String(args.segmentNotes == null ? '' :
args.segmentNotes);
const ratio = Math.max(0, Math.min(1, done / total));
const filled = Math.round(ratio * CELLS);
const bar = new Array(filled + 1).join('▰') + new Array(CELLS -
filled + 1).join('▱');
const percent = Math.round(ratio * 100);
const words = body.replace(/[#*>`\-]+/g, ' ').replace(/\s+/g, '
').trim();
const wordCount = words === '' ? 0 : words.split(' ').length;
const frames = (body.match(/^#### Frames? /gm) || []).length;
let status;
if (/NOT READ/.test(body)) {
status = '⚠️ not read';
} else if (/the reading separated \d+ of the/.test(body) || /did not
separate these/.test(body)) {
status = wordCount + ' words · 🔧 salvaged';
} else {
status = wordCount + ' words';
}
return {
line: '`' + bar + '` **' + done + '/' + total + '** · ' + label + ' · ' + status,
percent: percent,
words: wordCount,
frames: frames
};
param: progress
timeout: 30000
onFailure: ''
silent: true
- label: REPORT SEGMENT PROGRESS
type: say
message: '{{progress.line}}'
- label: APPEND SEGMENT RECORD
type: js
args: notes, segmentNotes, segment, range
code: >-
// FULL PAGE VISION · APPEND SEGMENT RECORD
// Adds one finished segment to the growing record. It looks like a
job for a CALC step, and
// it used to be one:
//
// calc set notes = '{{notes}}\n\n===== SEGMENT n
=====\n{{segmentNotes}}'
//
// That form is wrong for this particular value, for a documented
reason. The shaper armours
// the reading by rewriting `{{` to `{{\`, which is the engine's own
escape - and the engine
// defines that escape as consumed on interpolation: `{{\page}}` is
REPLACED with the literal
// `{{page}}`. A CALC value is interpolated. So the very first
append stripped the armour off
// the reading, and from the second segment onwards the record was
carried through the
// template engine unprotected, with `{{page}}` and `{{serp x}}`
lifted verbatim off the page
// sitting in it, live. Re-armouring at the end cannot undo a page
fetch that already happened.
//
// Arguments are the way out, and it is the same rule the frame
tokens already live by: a
// value that must not be interpolated travels as a JS argument.
Nothing here passes through
// the template engine, the armour is re-applied to the result, and
the escape is idempotent,
// so a record that arrives already armoured stays at exactly one
layer.
//
// It is also cheaper. The old form re-scanned the entire
accumulated record for parameters on
// every single segment - on a thirty segment walk that is a
quadratic amount of template
// scanning over a document that can pass half a million characters.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const previous = String(args.notes == null ? '' : args.notes);
const body = String(args.segmentNotes == null ? '' :
args.segmentNotes).trim();
const seg = Math.max(1, Number(String(args.segment ||
'').replace(/[^0-9]/g, '')) || 1);
let span = '';
try {
const r = args.range;
if (r && typeof r === 'object' && r.label) span = String(r.label);
else if (r) span = String(r);
} catch (e) { span = ''; }
const marker = '===== SEGMENT ' + seg + (span ? ' · ' + span : '') +
' =====';
const block = marker + '\n' + (body || '- **Gap:** nothing was
recorded for this segment.');
if (previous.trim() === '') return harden(block);
return harden(previous.replace(/\s+$/, '') + '\n\n' + block);
param: notes
timeout: 60000
onFailure: ''
silent: true
- label: DETECT REPEATED CHROME
type: js
args: notes
code: >-
// FULL PAGE VISION · DETECT REPEATED CHROME
// Finds the lines that come back on screen after screen: cookie
banners, navigation
// bars, sticky footers, share rails, and on serialised pages the
chapter or series
// header that is reprinted with every scroll. They are transcribed once
per frame, so
// on a hundred frame page they are paid for a hundred times - and paid
for again in
// every single answer, because the record is sent along with each one.
//
// This step only NOMINATES candidates. It never deletes anything,
because a repeated
// line is not automatically noise: a recurring table header or a price
that genuinely
// appears in every row is content. The judgement is left to the model
in the next step,
// and the surgery to the step after that.
function harden(value) {
// Text lifted off a page can contain the engine's own {{parameter}} syntax. Left as it is,
// it would be executed the next time this text is interpolated - fetching pages, running
// searches - once per occurrence and once per answer. The documented escape makes the
// braces literal without changing how the text reads. Idempotent by design.
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const raw = String(args.notes == null ? '' : args.notes);
const segments = raw.split(/^=====.*=====$/m).filter(function (s) {
return s.trim().length > 0; });
const total = segments.length;
const out = { count: 0, chars: 0, segments: total, list: '', ok: 'no' };
if (total < 3) return out;
function normalise(line) {
return line
.toLowerCase()
.replace(/[\u2018\u2019\u201c\u201d]/g, "'")
.replace(/[^a-z0-9\u00c0-\u024f\u0400-\u04ff\u0590-\u05ff\u0600-\u06ff\u4e00-\u9fff]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
const seen = {};
for (let i = 0; i < total; i++) {
const lines = segments[i].split('\n');
const here = {};
for (let j = 0; j < lines.length; j++) {
const original = lines[j].trim();
if (original.length < 3 || original.length > 200) continue;
const key = normalise(original);
if (key.length < 3) continue;
if (here[key]) continue;
here[key] = true;
if (!seen[key]) seen[key] = { hits: 0, sample: original, occurrences: 0, chars: 0 };
seen[key].hits += 1;
}
for (let j = 0; j < lines.length; j++) {
const original = lines[j].trim();
if (original.length < 3 || original.length > 200) continue;
const key = normalise(original);
if (key.length < 3 || !seen[key]) continue;
seen[key].occurrences += 1;
seen[key].chars += original.length + 1;
}
}
const minHits = Math.max(3, Math.ceil(total * 0.5));
const picked = [];
for (const key in seen) {
if (!Object.prototype.hasOwnProperty.call(seen, key)) continue;
const rec = seen[key];
if (rec.hits < minHits) continue;
picked.push({ key: key, sample: rec.sample, hits: rec.hits, chars: rec.chars - rec.sample.length - 1 });
}
picked.sort(function (a, b) { return b.chars - a.chars; });
const top = picked.slice(0, 40);
out.count = top.length;
out.chars = top.reduce(function (a, b) { return a + Math.max(0,
b.chars); }, 0);
out.list = harden(top.map(function (p) { return p.sample + ' [seen on
' + p.hits + ' of ' + total + ' screens]'; }).join('\n'));
out.ok = top.length > 0 ? 'yes' : 'no';
return out;
param: chrome
timeout: 30000
onFailure: ''
silent: true
- condition: '{{chrome.count}} > 0'
label: CLASSIFY REPEATED CHROME
type: gpt
prompt: >-
Please ignore all previous instructions. Answer in English regardless of
any other setting.
Below are lines that were transcribed from a long web page and came back
on most of its screens. Some of them are page furniture: navigation
bars, cookie or consent banners, sticky headers and footers, share
rails, advertisement labels, back-to-top buttons, or the series and
chapter title that a reading site reprints above every screen. Others
are genuine content that simply happens to repeat: a recurring table
header, a price that belongs to every row, a legal notice that carries
meaning, a heading that structures the document.
Decide for each line. Output one line per item you judge to be page
furniture, in this exact form and nothing else:
CHROME: <the line exactly as given, without the bracketed counter>
Rules:
- When in doubt, leave the line out. A wrongly kept line costs a few
tokens; a wrongly removed line costs information.
- Never output anything that carries a number, a date, a name or a
statement about the subject of the page.
- No preamble, no explanation, no closing remark. If none of them are
furniture, output nothing at all.
[CANDIDATE LINES]
{{chrome.list}}
[VERDICT]:
param: chromeVerdict
isolated: true
silent: true
- condition: '{{chrome.count}} > 0'
label: STRIP CLASSIFIED CHROME
type: js
args: notes, chromeVerdict
code: >-
// FULL PAGE VISION · STRIP CLASSIFIED CHROME
// Removes exactly the lines the model classified as page furniture and
keeps one copy
// of each in a PAGE CHROME block at the head of the record. Nothing is
destroyed: the
// text is still there, it is simply there once instead of once per
screen, so citations
// still resolve and the count of segments does not change.
//
// If the classification step returned nothing usable, this step is a
no-op and hands the
// record back untouched. A failed cleanup must never cost content.
function harden(value) {
// Text lifted off a page can contain the engine's own {{parameter}} syntax. Left as it is,
// it would be executed the next time this text is interpolated - fetching pages, running
// searches - once per occurrence and once per answer. The documented escape makes the
// braces literal without changing how the text reads. Idempotent by design.
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const raw = String(args.notes == null ? '' : args.notes);
const verdict = String(args.chromeVerdict == null ? '' :
args.chromeVerdict);
function normalise(line) {
return line
.toLowerCase()
.replace(/[\u2018\u2019\u201c\u201d]/g, "'")
.replace(/[^a-z0-9\u00c0-\u024f\u0400-\u04ff\u0590-\u05ff\u0600-\u06ff\u4e00-\u9fff]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
const kill = {};
const keptSamples = [];
const verdictLines = verdict.split('\n');
for (let i = 0; i < verdictLines.length; i++) {
let line = verdictLines[i].trim();
if (line.indexOf('CHROME:') !== 0) continue;
line = line.slice(7).trim().replace(/\s*\[seen on .*$/, '').trim();
const key = normalise(line);
if (key.length < 3) continue;
if (!kill[key]) {
kill[key] = true;
keptSamples.push(line);
}
}
if (keptSamples.length === 0) return raw;
const lines = raw.split('\n');
const out = [];
let removed = 0;
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed.length >= 3 && trimmed.length <= 200 && kill[normalise(trimmed)]) {
removed++;
continue;
}
out.push(lines[i]);
}
if (removed === 0) return raw;
const header = [
'===== PAGE CHROME · repeated on every screen, listed once =====',
''
].concat(keptSamples.map(function (s) { return '- ' + s;
})).concat(['']).join('\n');
return harden(header + out.join('\n').replace(/\n{4,}/g, '\n\n\n'));
param: notes
timeout: 30000
onFailure: ''
silent: true
- condition: '{{chrome.count}} > 0'
label: REPORT CHROME CLEANUP
type: say
message: >-
🧹 {{chrome.count}} repeated line(s) folded into one block
(~{{chrome.chars}} characters saved on every later answer).
- label: MEASURE RECORD
type: js
args: notes
code: >-
// FULL PAGE VISION · MEASURE RECORD
// Sizes the merged vision record so the command can decide whether a
single prompt can
// carry it or whether it has to be condensed slice by slice first.
const raw = String(args.notes == null ? '' : args.notes);
const body = raw.replace(/=====[^=]*=====/g, ' ').replace(/\s+/g, '
').trim();
const segments = (raw.match(/=====\s*SEGMENT\s+\d+/g) || []).length;
const gaps = (raw.match(/NOT READ/g) || []).length;
const chars = body.length;
return {
ok: chars > 0 ? 'yes' : 'no',
chars: chars,
words: body ? body.split(' ').length : 0,
tokens: Math.ceil(chars / 3.6),
segments: segments,
gaps: gaps
};
param: record
timeout: 30000
onFailure: ''
silent: true
- label: USE THE FULL RECORD
type: calc
func: set
param: payload
value: '{{notes}}'
format: ''
- label: REPORT RECORD
type: say
message: >-
✅ Record: {{shotCount}} frames · {{record.segments}} segment(s) ·
~{{record.words}} words
- condition: '{{failedSegments}} > 0'
label: WARN FAILED SEGMENTS
type: say
message: >-
⚠️ **{{failedSegments}}** segment(s) could not be read and are marked as
gaps in the record. Everything else is intact — pick RETRY in the
follow-up menu if you want the run repeated.
- label: HARDEN THE RECORD
type: group
steps:
- label: HARDEN PAYLOAD
type: js
args: payload
code: >-
// FULL PAGE VISION · HARDEN TEXT
// The one piece of armour this command cannot do without.
//
// The frames are read verbatim, so whatever stands on the page ends up
inside a parameter -
// and on a page that documents a template language, that includes
things like {{page}},
// {{serp query}} or {{transcript}}. The moment such a record is
interpolated into the next
// prompt, those look exactly like real system parameters: the engine
goes off and fetches a
// whole page, runs a web search or pulls a transcript, once per
occurrence, for every answer
// that carries the record. A documentation page with forty examples in
it turns a two second
// step into minutes of work that nobody asked for. It looks like a
hang; it is a record
// quoting the engine's own syntax back at it.
//
// The cure is the escape the engine itself defines: a backslash after
the opening braces
// marks them as literal. The text still reads as {{page}} for a human
and for the model, it
// simply stops being a command. Idempotent, so text that has already
been hardened is left
// alone rather than growing a second backslash on every pass.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
// The step is used on several different slots, so it accepts whichever
one it was given.
const candidates = ['payload', 'gpt', 'notes', 'text', 'condensed',
'segmentNotes'];
let raw = '';
for (let i = 0; i < candidates.length; i++) {
if (args[candidates[i]] != null && String(args[candidates[i]]) !== '') {
raw = args[candidates[i]];
break;
}
}
return harden(raw);
param: payload
timeout: 30000
onFailure: ''
silent: true
- label: HARDEN NOTES
type: js
args: notes
code: >-
// FULL PAGE VISION · HARDEN TEXT
// The one piece of armour this command cannot do without.
//
// The frames are read verbatim, so whatever stands on the page ends up
inside a parameter -
// and on a page that documents a template language, that includes
things like {{page}},
// {{serp query}} or {{transcript}}. The moment such a record is
interpolated into the next
// prompt, those look exactly like real system parameters: the engine
goes off and fetches a
// whole page, runs a web search or pulls a transcript, once per
occurrence, for every answer
// that carries the record. A documentation page with forty examples in
it turns a two second
// step into minutes of work that nobody asked for. It looks like a
hang; it is a record
// quoting the engine's own syntax back at it.
//
// The cure is the escape the engine itself defines: a backslash after
the opening braces
// marks them as literal. The text still reads as {{page}} for a human
and for the model, it
// simply stops being a command. Idempotent, so text that has already
been hardened is left
// alone rather than growing a second backslash on every pass.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
// The step is used on several different slots, so it accepts whichever
one it was given.
const candidates = ['payload', 'gpt', 'notes', 'text', 'condensed',
'segmentNotes'];
let raw = '';
for (let i = 0; i < candidates.length; i++) {
if (args[candidates[i]] != null && String(args[candidates[i]]) !== '') {
raw = args[candidates[i]];
break;
}
}
return harden(raw);
param: notes
timeout: 30000
onFailure: ''
silent: true
- condition: '{{record.chars}} > 60000'
label: CHOOSE HOW THE RECORD IS READ
type: group
steps:
- label: INDEX THE RECORD INTO PASSAGES
type: js
args: payload
code: >-
// FULL PAGE VISION · PASSAGE INDEX
// Cuts the finished record into numbered passages so that a later
request can be given the
// part it needs instead of the whole thing.
//
// The cut follows the record's own seams. Every passage is exactly one
`#### Frame N` block,
// which means a passage never straddles two frames and the frame number
is therefore an
// honest citation: a reader can scroll back to that screen and check
the claim. A frame that
// is longer than MAX_PASSAGE on its own is split into parts that keep
the same frame number,
// because the alternative - a passage that quietly spans frames 7 to 9
- would make every
// citation a guess.
//
// Nothing is thrown away here. The index is a second view of the
record, not a replacement:
// the full record stays in `payload` and is still what the export
writes and what FULL RECORD
// sends.
const MAX_PASSAGE = 2400;
const raw = String(args.payload == null ? '' : args.payload);
if (raw.trim() === '') {
return { chunks: [], totalChunks: 0, totalChars: 0, frames: 0, ok: 'no' };
}
// Frame headings are the seam. Segment markers are dropped: they carry
no content and would
// otherwise become passages of their own.
const parts = raw.split(/\n(?=#### Frames? )/);
const chunks = [];
let frames = 0;
let totalChars = 0;
for (let p = 0; p < parts.length; p++) {
const block = parts[p].replace(/^={3,}[^\n]*={3,}\n?/gm, '').trim();
if (block === '') continue;
const head = block.match(/^#### (Frames? [\d\-]+)/);
const label = head ? head[1] : 'Preamble';
if (head) frames++;
totalChars += block.length;
if (block.length <= MAX_PASSAGE) {
chunks.push({ id: 'P' + (chunks.length + 1), frame: label, text: block, part: 0 });
continue;
}
// A long frame is cut on bullet boundaries, never mid-bullet: a half bullet is a half fact.
const lines = block.split('\n');
let buf = '';
let part = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (buf !== '' && buf.length + line.length + 1 > MAX_PASSAGE) {
chunks.push({
id: 'P' + (chunks.length + 1),
frame: label,
text: (part > 1 ? '#### ' + label + ' (part ' + part + ')\n' : '') + buf.trim(),
part: part
});
part++;
buf = '';
}
buf += (buf === '' ? '' : '\n') + line;
}
if (buf.trim() !== '') {
chunks.push({
id: 'P' + (chunks.length + 1),
frame: label,
text: (part > 1 ? '#### ' + label + ' (part ' + part + ')\n' : '') + buf.trim(),
part: part
});
}
}
return {
chunks: chunks,
totalChunks: chunks.length,
totalChars: totalChars,
frames: frames,
ok: chunks.length > 0 ? 'yes' : 'no'
};
param: passages
timeout: 60000
onFailure: ''
silent: true
- label: READ THE RECORD SIZE
type: calc
func: set
param: recordChars
value: '{{record.chars}}'
format: ''
- label: READ THE PASSAGE COUNT
type: calc
func: set
param: passageCount
value: '{{passages.totalChunks}}'
format: ''
- label: READ THE FRAME COUNT
type: calc
func: set
param: frameCount
value: '{{passages.frames}}'
format: ''
- label: WEIGH THE TWO METHODS
type: js
args: recordChars, passageCount, frameCount
code: >-
// FULL PAGE VISION · WHICH READING METHOD
// Two ways to put a large record in front of a model, and a
recommendation between them.
//
// WHAT THIS IS NOT ABOUT. It is not about the context window. Current
models take hundreds of
// thousands of tokens, some of them millions, and a record of this size
is nowhere near any
// of those limits. An earlier version of this hint claimed the request
would be "cut off at
// the end". That was simply false, and a false reason for a real
recommendation is worse than
// no reason at all.
//
// WHAT IT IS ABOUT. Accuracy falls as the context grows, long before
the window does. The
// effect is measured and consistent across model families: information
in the middle of a
// long input is retrieved far less reliably than information at either
end, and the drop is
// steep - the same models that answer near-perfectly at a couple of
thousand tokens lose a
// large part of that accuracy by the tens of thousands. The usable
context is reliably
// shorter than the advertised one.
//
// So the trade is not "fits" against "does not fit". It is:
//
// FULL RECORD everything is visible, and nothing is chosen for the
model - but every fact
// it needs is buried among tens of thousands of tokens
it does not.
// RETRIEVAL only the passages this request needs travel, so the
thing being asked about
// sits in a short context instead of in the middle of a
long one - and each
// passage carries the frame it came from, so the answer
can be checked.
//
// Below FLIP the record is short enough that position effects stay
small and seeing
// everything wins. Above it, a short focused context wins.
const FLIP = 80000;
const chars = Math.max(0, Number(args.recordChars) || 0);
const passages = Math.max(0, Number(args.passageCount) || 0);
const frames = Math.max(0, Number(args.frameCount) || 0);
const tokens = Math.round(chars / 3.6);
const recommended = chars > FLIP ? 'retrieval' : 'full';
function n(v) { return v.toLocaleString('en-US'); }
const head = '🧮 The record is **' + n(chars) + '** characters (~' +
n(tokens) +
' tokens) across **' + n(frames) + '** frame(s), indexed into **' + n(passages) +
'** passages.';
const why = '\n\nThis is well inside any current context window — size
is not the problem. ' +
'Accuracy is: retrieval quality falls as the context grows, and anything sitting in the ' +
'middle of a long input is the first thing a model loses.';
const verdict = recommended === 'retrieval'
? '\n\n💡 **Retrieval is recommended at this length.** A short, focused context is read more ' +
'reliably than a long one, and every passage names the frame it came from, so each claim ' +
'can be checked against a screen.'
: '\n\n💡 **Full record is recommended at this length.** It is still short enough that ' +
'position effects stay small, and a model that sees every frame beats one that sees a ' +
'good selection.';
return { recommended: recommended, chars: chars, tokens: tokens,
passages: passages,
frames: frames, hint: head + why + verdict };
param: methodHint
timeout: 30000
onFailure: ''
silent: true
- condition: '{{methodHint.recommended}} = full'
label: OFFER THE FULL RECORD FIRST
type: ask
message: >-
{{methodHint.hint}}
**📄 FULL RECORD** sends every frame, in order, in one request.
**🔬 RETRIEVAL** cuts the record into passages at the frame boundaries,
ranks them against the words of each request, widens that with the terms
the record itself uses around the best hits, and sends only the
strongest — each one labelled with the frame it came from. Quotes are
checked against the record afterwards and corrected.
param: method
options:
- label: 📄 FULL RECORD · every frame in a single request
value: full
- label: 🔬 RETRIEVAL · only the passages each request needs
value: retrieval
default: full
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- condition: '{{methodHint.recommended}} = retrieval'
label: OFFER RETRIEVAL FIRST
type: ask
message: >-
{{methodHint.hint}}
**📄 FULL RECORD** sends every frame, in order, in one request.
**🔬 RETRIEVAL** cuts the record into passages at the frame boundaries,
ranks them against the words of each request, widens that with the terms
the record itself uses around the best hits, and sends only the
strongest — each one labelled with the frame it came from. Quotes are
checked against the record afterwards and corrected.
param: method
options:
- label: 🔬 RETRIEVAL · only the passages each request needs
value: retrieval
- label: 📄 FULL RECORD · every frame in a single request
value: full
default: retrieval
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: SETTLE THE READING METHOD
type: js
args: method, methodHint
code: |
const m = String(args.method || '').trim().toLowerCase();
if (m === 'full' || m === 'retrieval') return m;
try {
const r = args.methodHint;
if (r && typeof r === 'object' && r.recommended === 'full') return 'full';
} catch (e) { /* fall through */ }
return 'retrieval';
param: method
timeout: 30000
onFailure: ''
silent: true
- condition: '{{method}} = retrieval'
label: TURN ON THE CITATIONS
type: calc
func: set
param: citationRule
value: >
[CITING THE EVIDENCE]
The evidence above is a SELECTION from the record, not all of it. Every
passage carries a number in square brackets.
- Close every distinct claim with its own citation, written `[N]` with
the passage number and nothing else. One claim, one citation; never
combine numbers into `[1, 2]`, and never write a frame name next to the
number.
- Use only numbers that appear in the evidence.
- End with a `**Source References**` block: one blockquote per distinct
citation you used, holding a quote copied character for character out of
the evidence, followed by `**[N]**`. Never paraphrase inside a quote and
never shorten one with an ellipsis — every quote is located in the
record afterwards, restored to its exact wording and re-pointed at the
passage it truly sits in, so copying exactly is always in your interest.
- If the request asks for code, a file, a document or a page rebuild,
put NO citation markers inside the artifact itself — output it clean,
and place the Source References block after it.
- What this selection does not cover, name as not covered. Never fill it
from anywhere else.
format: ''
- condition: '{{method}} = full'
label: TURN OFF THE CITATIONS
type: calc
func: set
param: citationRule
value: ''
format: ''
- condition: '{{method}} = full'
label: USE THE WHOLE RECORD AS EVIDENCE
type: group
steps:
- label: COPY THE RECORD INTO THE EVIDENCE SLOT
type: calc
func: set
param: evidence
value: '{{payload}}'
format: ''
- label: HARDEN THE EVIDENCE
type: js
args: evidence
code: >-
// FULL PAGE VISION · RE-ARMOUR THE EVIDENCE
// The evidence arrives through a CALC step, and a CALC value is
interpolated, which the
// engine defines as consuming one layer of the `{{\` escape. So the
text that reaches this
// step has none left, and the prompt that receives it next would
execute any `{{page}}` or
// `{{serp x}}` the page happened to display. One layer goes back on,
and the escape is
// idempotent, so a value that still had one keeps exactly one.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
return harden(args.evidence);
param: evidence
timeout: 60000
onFailure: ''
silent: true
- condition: '{{pendingAnswer}} = yes'
label: PRESELECT FOCUSED ANSWER
type: group
steps:
- label: DISARM THE INSTRUCTION ANSWER
type: calc
func: set
param: pendingAnswer
value: 'no'
format: ''
- label: ROUTE TO THE FOCUSED ANSWER
type: calc
func: set
param: format
value: focus
format: ''
- label: SKIP THE OUTPUT MENU
type: jump
to: RESET OUTPUT STATE
- label: SELECT OUTPUT
type: ask
message: >-
The whole page has been seen. What should be produced from it? Pick one, or
type your own instruction or question about the page.
param: format
options:
- label: ⚡️ QUICK · key takeaway plus summary
value: quick
- label: 📖 FULL · long-form report over the entire record
value: full
- label: 🧾 STRUCTURED · sectioned report with facts and figures
value: structured
- label: 🖼️ VISUALS · what the images, charts and diagrams show
value: visuals
- label: 📊 DATA · every number, table and chart as markdown tables
value: data
- label: 🎨 UI AUDIT · layout, hierarchy, calls to action, accessibility
value: audit
- label: 🖨️ REBUILD · the whole page rebuilt as a markdown document
value: rebuild
- label: 🧭 LINKS · every link, button and form field in its section
value: links
- label: 🗨️ QUOTING · verbatim citations with segment anchors
value: quoting
- label: 📜 RECORD · show the frame-by-frame reading itself
value: record
- label: 📦 EXPORT · download the answer or the full record
value: export
- label: ♻️ REPURPOSE · hand over to the Repurpose text command
value: repurpose
- value: $custom
default: quick
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: RESET OUTPUT STATE
type: group
steps:
- label: CLEAR FOLLOW UP STATE
type: calc
func: set
param: change
value: ''
format: ''
- label: CLEAR PREVIOUS ANSWER
type: calc
func: set
param: gpt
value: ''
format: ''
- condition: '{{method}} = retrieval'
label: PREPARE THE EVIDENCE
type: group
steps:
- label: BUILD THE RETRIEVAL QUERY
type: js
args: format, focus, change
code: >-
// FULL PAGE VISION · WHAT THIS REQUEST IS LOOKING FOR
// Turns the request at hand into something the index can be searched
with. Three sources, in
// order of precedence: a follow-up the user just typed, the instruction
given before the
// reading, and otherwise the output mode that was picked from the menu.
//
// The output modes are not questions, so they cannot be searched for as
if they were. Each
// one is described by the record labels it lives in and by the words
the record uses inside
// those labels - `**Visual:**` for a visual analysis, `**Table:**` and
`**Data:**` for a data
// extraction - and the ones that are inherently about the whole page
are marked `spread`,
// which makes the selector sample evenly across every frame instead of
piling up wherever the
// keywords happen to cluster. A summary built from the top of a ranking
is not a summary.
function clean(v) {
return String(v == null ? '' : v).replace(/\s+/g, ' ').trim();
}
const MODES = {
quick: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
full: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
structured: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
rebuild: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
quoting: { intent: 'SUMMARY', spread: true, labels: ['Text', 'Heading'], text: '' },
visuals: { intent: 'TARGET', spread: false, labels: ['Visual'],
text: 'image illustration photograph icon logo avatar chart diagram map screenshot figure colour layout typeface' },
data: { intent: 'TARGET', spread: false, labels: ['Table', 'Data'],
text: 'number value price date percent currency unit table row column axis series total amount rating count' },
audit: { intent: 'TARGET', spread: false, labels: ['UI'],
text: 'button link tab menu field form input checkbox dropdown search navigation label placeholder state disabled' },
links: { intent: 'TARGET', spread: false, labels: ['UI'],
text: 'link button action navigation download tab menu href anchor call to action' }
};
// The values the two menus can put into these slots. A menu value is a
routing token, not
// something to search the record for: retrieving the passages that
contain the word "shorten"
// would answer a question nobody asked.
const MENU = {
shorten: 1, simplify: 1, clarify: 1, expand: 1, translate: 1, factCheck: 1,
export: 1, repurpose: 1, return: 1, retry: 1, no: 1, record: 1, focus: 1
};
const followUp = clean(args.change);
const mode = clean(args.format).toLowerCase();
// A follow-up the user just typed outranks everything: it is the newest
thing they said.
if (followUp !== '' &&
!Object.prototype.hasOwnProperty.call(MENU, followUp) &&
!Object.prototype.hasOwnProperty.call(MODES, followUp)) {
return { text: followUp, intent: 'QUERY', spread: false, labels: [], skip: false,
source: 'follow-up' };
}
// The instruction given before the reading, when the focused answer is
what is being produced.
let request = '';
try {
const f = args.focus;
if (f && typeof f === 'object' && f.request) request = clean(f.request);
} catch (e) { request = ''; }
if (mode === 'focus' && request !== '') {
return { text: request, intent: 'QUERY', spread: false, labels: [], skip: false,
source: 'instruction' };
}
// Two outputs never read the evidence at all: SHOW THE RECORD prints
the whole record and
// the export writes it to a file. Running a selection for them would
burn a BM25 pass over
// every passage to produce something nobody looks at.
if (mode === 'record' || mode === 'export') {
return { text: '', intent: 'NONE', spread: true, labels: [], skip: true,
source: 'not needed' };
}
const preset = Object.prototype.hasOwnProperty.call(MODES, mode) ?
MODES[mode] : null;
if (preset) {
// The instruction from before the reading still colours a preset output, because it is
// what the record was written to serve.
const text = (preset.text + ' ' + request).trim();
return { text: text, intent: preset.intent, spread: preset.spread,
labels: preset.labels, skip: false, source: 'output mode' };
}
// Anything else in the output menu is a free instruction typed by the
user.
if (mode !== '') {
return { text: clean(args.format), intent: 'QUERY', spread: false, labels: [],
skip: false, source: 'instruction' };
}
return { text: request, intent: 'SUMMARY', spread: true, labels: [],
skip: false,
source: 'whole record' };
param: query
timeout: 30000
onFailure: ''
silent: true
- label: SELECT THE EVIDENCE
type: js
args: passages, query, payload
code: >-
// FULL PAGE VISION · EVIDENCE SELECTION
// Picks the passages this one request needs and lays them out with a
map that binds every
// passage number to the frame it came from.
//
// The scoring is BM25 over the passage index, run once per keyword set
and fused by
// reciprocal rank, which is the standard way to combine several
rankings without having to
// make their scores comparable. Three sets are used:
//
// A · the request's own words.
// B · dictionary-free prefix stems of them, so an inflected language
still matches -
// "Schriftarten" in the request finds "Schriftart" in the record
without anybody
// shipping a stemmer.
// C · the words the record itself uses around the first-pass hits.
This is the classic
// pseudo-relevance feedback step: take the strongest passages
from pass one, find the
// terms that are common inside them and rare everywhere else, and
search again with
// those. It is the same job the source command gives to a model -
"keywords that would
// appear inside the ANSWER passage itself, phrased the way this
corpus phrases things" -
// except that here the corpus is asked instead of guessed, which
costs nothing, cannot
// hallucinate a term, and is automatically in the record's own
language and wording.
//
// Three things make this different from a plain search:
//
// * LABEL BOOST. Our passages are not prose, they are labelled
bullets. A data extraction
// wants the passages carrying `**Table:**` and `**Data:**`, and
saying so directly beats
// hoping the word "table" happens to appear.
//
// * SPREAD. A summary, a report or a page rebuild is about the whole
page. Ranking cannot
// answer that question - the top of any ranking is wherever the
keywords cluster - so
// these sample evenly across the frames instead, from the first to
the last.
//
// * NEIGHBOURS. A bullet cut at a passage border finishes in the next
one. After the
// selection is made, a few immediate neighbours of the strongest
hits are pulled in, in
// reading order, so a `**Cut:**` never arrives without its other
half.
//
// The output is always in reading order, never in score order: the
record's own sequence is
// information, and a model handed frames 12, 3, 40, 7 has to
reconstruct it before it can use
// anything.
const BUDGET = 40000;
const RRF_K = 60;
const MAX_PICK = 60;
function norm(s) {
return String(s == null ? '' : s)
.normalize('NFKC')
.replace(/[\u2010-\u2015\u2212]/g, '-')
.replace(/[\u2018\u2019\u201C\u201D\u00AB\u00BB]/g, '"')
.replace(/\s+/g, ' ')
.toLowerCase()
.trim();
}
const CJK =
/[\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\u0E00-\u0E7F]/;
function words(s) {
const out = [];
const parts = norm(s).split(/[^\p{L}\p{N}\p{M}]+/u);
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (!p) continue;
if (CJK.test(p)) {
if (p.length <= 2) { out.push(p); continue; }
for (let j = 0; j + 2 <= p.length; j++) out.push(p.substr(j, 2));
} else if (p.length >= 3) {
out.push(p);
}
}
return out;
}
function uniq(list, cap) {
const seen = {}, out = [];
for (let i = 0; i < list.length; i++) {
const k = list[i];
if (!k || seen[k]) continue;
seen[k] = 1;
out.push(k);
if (cap && out.length >= cap) break;
}
return out;
}
// A hit only counts when the term starts at a word boundary. Without
that guard "art" scores
// on "start", "particular" and "Artefakte", and a request about
typefaces retrieves the
// sidebar.
const WORDCH = /[0-9\p{L}\p{M}]/u;
function tf(hay, kw) {
let n = 0, i = 0;
while ((i = hay.indexOf(kw, i)) !== -1) {
if (i === 0 || !WORDCH.test(hay.charAt(i - 1))) n++;
i += kw.length;
}
return n;
}
let index = { chunks: [], totalChunks: 0, totalChars: 0 };
try {
const p = args.passages;
if (p && typeof p === 'object' && p.chunks) index = p;
} catch (e) { /* fall through to the whole record */ }
const chunks = index.chunks || [];
const fallback = String(args.payload == null ? '' : args.payload);
if (!chunks.length) {
return { text: fallback, stats: '', picked: 0, total: 0, ok: 'no', mode: 'whole record' };
}
let q = { text: '', intent: 'SUMMARY', spread: true, labels: [] };
try {
const raw = args.query;
if (raw && typeof raw === 'object') q = raw;
} catch (e) { /* keep the default */ }
const labels = Array.isArray(q.labels) ? q.labels : [];
const spread = q.spread === true || q.spread === 'true';
// Some outputs do not read the evidence at all. Selecting for them
would be a full BM25 pass
// over every passage to produce something nobody looks at.
if (q.skip === true || q.skip === 'true') {
return { text: fallback, stats: '', picked: 0, total: 0, ok: 'no', mode: 'not needed',
views: 0, expanded: 0 };
}
const N = chunks.length;
const lower = chunks.map(function (c) { return norm(c.text); });
const lens = chunks.map(function (c) { return String(c.text).length; });
let avgdl = 0;
for (let i = 0; i < N; i++) avgdl += lens[i];
avgdl = avgdl / Math.max(N, 1);
// Which passages carry the labels this output lives in.
const labelHit = chunks.map(function (c) {
if (!labels.length) return false;
for (let i = 0; i < labels.length; i++) {
if (String(c.text).indexOf('**' + labels[i] + ':**') !== -1) return true;
}
return false;
});
function rank(kws) {
const k1 = 1.5, b = 0.75, df = {};
kws.forEach(function (kw) {
let c = 0;
for (let i = 0; i < N; i++) if (lower[i].indexOf(kw) !== -1) c++;
df[kw] = c;
});
const scored = [];
for (let i = 0; i < N; i++) {
let sc = 0;
for (let j = 0; j < kws.length; j++) {
const t = tf(lower[i], kws[j]);
if (t <= 0) continue;
const idf = Math.log((N - df[kws[j]] + 0.5) / (df[kws[j]] + 0.5) + 1);
sc += idf * (t * (k1 + 1)) / (t + k1 * (1 - b + b * lens[i] / avgdl));
}
if (sc > 0) scored.push({ i: i, s: sc });
}
scored.sort(function (a, b2) { return b2.s - a.s; });
return scored;
}
const surface = uniq(words(q.text || ''), 24);
const stems = uniq(surface.map(function (w) {
if (w.length < 6) return '';
const cut = Math.max(5, Math.ceil(w.length * 0.75));
return cut < w.length ? w.slice(0, cut) : '';
}).filter(Boolean), 24).filter(function (x) { return surface.indexOf(x)
=== -1; });
const sets = [];
if (surface.length) sets.push(surface);
if (stems.length) sets.push(stems);
const rrf = {};
let bestRaw = 0;
let views = 0;
let firstPass = [];
function fuse(kws) {
const ranked = rank(kws);
if (!ranked.length) return [];
if (ranked[0].s > bestRaw) bestRaw = ranked[0].s;
for (let r = 0; r < ranked.length && r < 80; r++) {
rrf[ranked[r].i] = (rrf[ranked[r].i] || 0) + 1 / (RRF_K + r + 1);
}
views++;
return ranked;
}
for (let si = 0; si < sets.length; si++) {
const ranked = fuse(sets[si]);
if (si === 0) firstPass = ranked;
}
// Set C · pseudo-relevance feedback. Only for a question somebody
actually typed: a canned
// word list for a preset output has nothing to expand, and expanding a
spread query would
// pull the sample towards whatever the first pass happened to like.
let expansion = [];
if (String(q.intent || '') === 'QUERY' && firstPass.length) {
const top = firstPass.slice(0, 5).map(function (x) { return x.i; });
const inTop = {};
top.forEach(function (i) {
const seen = {};
words(chunks[i].text).forEach(function (w) {
if (seen[w]) return;
seen[w] = 1;
inTop[w] = (inTop[w] || 0) + 1;
});
});
const already = {};
sets.forEach(function (set) { set.forEach(function (w) { already[w] = 1; }); });
const cand = [];
Object.keys(inTop).forEach(function (w) {
if (already[w] || w.length < 4 || inTop[w] < 2) return;
let df = 0;
for (let i = 0; i < N; i++) if (lower[i].indexOf(w) !== -1) df++;
if (df === 0 || df > N * 0.5) return;
cand.push({ w: w, s: inTop[w] * Math.log(N / df) });
});
cand.sort(function (a, b2) { return b2.s - a.s; });
expansion = cand.slice(0, 12).map(function (c) { return c.w; });
if (expansion.length) fuse(expansion);
}
// The label boost is additive and deliberately large enough to outrank
a weak lexical hit:
// for a labelled output the label IS the query, and the words are only
a tie-breaker.
if (labels.length) {
for (let i = 0; i < N; i++) {
if (labelHit[i]) rrf[i] = (rrf[i] || 0) + 1 / RRF_K;
}
}
let fused = Object.keys(rrf).map(function (k) {
return { i: parseInt(k, 10), s: rrf[k] };
});
fused.sort(function (a, b2) { return b2.s - a.s; });
let picked = [];
let mode;
if (spread || !fused.length) {
// Even coverage of the whole record, in reading order, as many as the budget allows.
// "no lexical match" is only honest when there were words to match with: a summary has no
// question behind it, so nothing failed - it was never a search in the first place.
mode = fused.length ? 'spread'
: (sets.length ? 'spread (no lexical match)' : 'spread (whole record)');
const step = Math.max(1, Math.floor(N / Math.max(1, Math.min(MAX_PICK, N))));
for (let i = 0; i < N && picked.length < MAX_PICK; i += step) picked.push(i);
// The first and the last frame anchor a summary: the page starts and ends somewhere.
if (picked.indexOf(0) === -1) picked.unshift(0);
if (picked.indexOf(N - 1) === -1) picked.push(N - 1);
// Strong lexical hits are added on top, because a spread is a floor and not a ceiling.
for (let f = 0; f < fused.length && f < 12; f++) {
if (picked.indexOf(fused[f].i) === -1) picked.push(fused[f].i);
}
} else {
mode = labels.length ? 'labelled + ranked' : 'ranked';
for (let f = 0; f < fused.length && picked.length < MAX_PICK; f++) picked.push(fused[f].i);
}
// Neighbour recovery for the strongest hits, so a bullet cut at a
border keeps its other half.
const chosen = {};
picked.forEach(function (i) { chosen[i] = 1; });
let grown = 0;
for (let p = 0; p < picked.length && grown < 8; p++) {
const i = picked[p];
if (i > 0 && !chosen[i - 1]) { chosen[i - 1] = 1; picked.push(i - 1); grown++; }
if (grown >= 8) break;
if (i + 1 < N && !chosen[i + 1]) { chosen[i + 1] = 1; picked.push(i + 1); grown++; }
}
// Budget in order of strength, then restore reading order.
const strength = {};
picked.forEach(function (i, r) { strength[i] = r; });
picked.sort(function (a, b2) { return strength[a] - strength[b2]; });
const selected = [];
let used = 0;
for (let p = 0; p < picked.length; p++) {
const i = picked[p];
if (used + lens[i] > BUDGET && selected.length > 0) continue;
selected.push(i);
used += lens[i];
}
selected.sort(function (a, b2) { return a - b2; });
if (!selected.length) {
return { text: fallback, stats: '', picked: 0, total: N, ok: 'no', mode: 'whole record' };
}
let map = '';
let body = '';
selected.forEach(function (i, k) {
const n = k + 1;
map += '[' + n + '] = ' + chunks[i].frame + '\n';
body += '[' + n + '] ' + chunks[i].text + '\n\n---\n\n';
});
const text =
'━━━ EVIDENCE MAP ━━━\n' + map +
'━━━━━━━━━━━━━━━━━━━━\n\n' + body.replace(/\n+---\n+$/, '\n');
const gaps = N - selected.length;
const stats = '`' + selected.length + '/' + N + ' passages · ' +
used.toLocaleString('en-US') + ' chars · ' + mode + ' · ' + views +
' query view' + (views === 1 ? '' : 's') +
(expansion.length ? ' · expanded' : '') +
(gaps > 0 ? ' · ' + gaps + ' passage(s) not sent' : ' · complete') + '`';
return { text: text, stats: stats, picked: selected.length, total: N,
ok: 'yes',
mode: mode, views: views, expanded: expansion.length };
param: selection
timeout: 60000
onFailure: ''
silent: true
- label: ADOPT THE SELECTED EVIDENCE
type: calc
func: set
param: evidence
value: '{{selection.text}}'
format: ''
- label: HARDEN THE SELECTED EVIDENCE
type: js
args: evidence
code: >-
// FULL PAGE VISION · RE-ARMOUR THE EVIDENCE
// The evidence arrives through a CALC step, and a CALC value is
interpolated, which the
// engine defines as consuming one layer of the `{{\` escape. So the
text that reaches this
// step has none left, and the prompt that receives it next would
execute any `{{page}}` or
// `{{serp x}}` the page happened to display. One layer goes back on,
and the escape is
// idempotent, so a value that still had one keeps exactly one.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
return harden(args.evidence);
param: evidence
timeout: 60000
onFailure: ''
silent: true
- label: REPORT THE SELECTION
type: say
message: 🔎 {{selection.stats}}
- condition: '{{format}} = focus'
label: FOCUSED ANSWER
type: group
steps:
- label: GENERATE FOCUSED ANSWER
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{buildRule}}
Whatever stands under [YOUR REQUEST] is what you have to do with the
record. The frames were read with this request already in mind, so the
record was written to serve it.
[RULES]
- Do what the request says and output the result alone: no preamble, no
account of what you did, no closing remark. Where it asks for code or a
document, the answer is that code or that document and nothing else.
- Never reply that the record "does not contain" what was asked for. The
record contains the page; what was asked for is what you build out of
it. A request to build something can always be carried out, because the
material is already there.
- Take every word, number, heading, label, price, name and image
description from the record and reproduce it exactly.
- Use every piece of visual form the record gives you - typefaces,
sizes, colours, spacing, alignment, layout, order. Where the record is
silent, choose something consistent with what it does say and carry on;
do not stop to ask and do not leave a placeholder for a decision you can
make yourself.
- Where the record marks a region `[unreadable]` or `NOT READ`, put a
clearly marked placeholder at that spot and continue. A gap never stops
the work.
- If the request is a question rather than a task, answer it from the
record and name the frame it came from.
[YOUR REQUEST]:
{{focus.request}}
[EVIDENCE]:
{{citationRule}}{{evidence}}
[ANSWER]:
param: gpt
isolated: true
- label: AFTER FOCUSED ANSWER
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = quick'
label: QUICK SUMMARY
type: group
steps:
- label: GENERATE QUICK SUMMARY
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
[REPORT FORMAT]
### Key Takeaway
The single most important point, one sentence.
### Summary
Bullet points covering every substantive idea, fact and figure on the
page, including what the visuals contribute. No word limit; completeness
beats brevity, but do not pad. If the page holds a dialogue or thread,
extract the main discussion points and name the most active
participants. Do not use emoji.
**Related queries:**
```markdown
Short related query
```
```markdown
Short related query
```
```markdown
Short related query
```
[EVIDENCE]:
{{citationRule}}{{evidence}}
[REPORT FOLLOWED BY RELATED QUERIES]:
param: gpt
isolated: true
- label: AFTER QUICK SUMMARY
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = full'
label: FULL REPORT
type: group
steps:
- label: GENERATE FULL REPORT
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
Write the long-form report. Work through the record from the first
segment to the last so that no part of the page is under-represented.
[REPORT FORMAT]
### Key Takeaway
One sentence.
### Executive Summary
Five to nine bullets.
### Detailed Findings
Grouped by theme, one `####` subheading per theme, bullets underneath.
Name the segment a finding came from where it helps.
### What The Visuals Show
Bullets covering images, charts, diagrams and layout signals.
### Numbers, Dates And Named Entities
A markdown table with the columns Item, Value, Context.
### Open Questions And Unreadable Regions
Bullets. Include anything the record marks as `[unreadable]` or `NOT
READ`.
**Related queries:**
```markdown
Short related query
```
```markdown
Short related query
```
```markdown
Short related query
```
[EVIDENCE]:
{{citationRule}}{{evidence}}
[FINAL REPORT]:
param: gpt
isolated: true
- label: AFTER FULL REPORT
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = structured'
label: STRUCTURED REPORT
type: group
steps:
- label: GENERATE STRUCTURED REPORT
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
Turn the record into a structured analyst report. Every line must be
traceable to it. Leave a section out entirely if the page contains
nothing for it.
[REPORT FORMAT]
### Subject And Purpose
### Core Statements
### Facts And Figures
Markdown table: Item | Value | Context
### Visual Inventory
Markdown table: Element | Type | What it shows
### Named Entities
Markdown table: Name | Type | Role on the page
### Actions, Offers And Calls To Action
### Gaps, Caveats And Unreadable Regions
**Related queries:**
```markdown
Short related query
```
```markdown
Short related query
```
```markdown
Short related query
```
[EVIDENCE]:
{{citationRule}}{{evidence}}
[STRUCTURED REPORT]:
param: gpt
isolated: true
- label: AFTER STRUCTURED REPORT
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = visuals'
label: VISUAL ANALYSIS
type: group
steps:
- label: GENERATE VISUAL ANALYSIS
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
Report exclusively on what was *seen* rather than read: photographs,
illustrations, icons, charts, diagrams, maps, screenshots, layout and
visual hierarchy. This is the part a plain text extraction would have
thrown away.
[REPORT FORMAT]
### What The Page Looks Like
Two or three sentences on layout, structure and visual tone.
### Visual Elements
One `####` block per element: what it depicts, what it communicates,
where it sits and how it relates to the surrounding text.
### Data In Charts And Diagrams
Axes, series and readable values as a markdown table where possible.
Write "none present" if there are no charts.
### Anything Unreadable
Regions the record marks as cut off, blurred or illegible.
[EVIDENCE]:
{{citationRule}}{{evidence}}
[VISUAL ANALYSIS]:
param: gpt
isolated: true
- label: AFTER VISUAL ANALYSIS
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = data'
label: DATA EXTRACTION
type: group
steps:
- label: GENERATE DATA EXTRACTION
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
Pull every quantitative thing on the page out of the record and lay it
out as data. Charts count: the record read their axes, series and labels
off the pixels, and a plain text extraction could never have done that.
[RULES]
- One markdown table per source. Put the source above it as a `####`
heading, e.g. `#### Pricing table (segment 2)`.
- Reproduce every number, unit, currency and date exactly as recorded.
Never round, never convert, never re-order rows.
- For charts and diagrams, give the columns Series, Category, Value,
Unit. Mark any value that was read off a bar or a line rather than
printed as a label with `~` in front of it.
- For specification lists, key-value blocks, pricing tiers and
comparison grids, use the columns Item, Value, Context.
- Where a cell was unreadable write `[unreadable]`, never a guess.
- After the tables add a short `### Notes` section: units, time frames,
footnotes, and anything that makes the numbers comparable or not
comparable.
- If the record contains no quantitative content at all, say exactly
that in one sentence and stop.
[EVIDENCE]:
{{citationRule}}{{evidence}}
[DATA TABLES]:
param: gpt
isolated: true
- label: AFTER DATA EXTRACTION
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = audit'
label: UI AUDIT
type: group
steps:
- label: GENERATE UI AUDIT
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
Audit this page the way a designer would who is looking at a screenshot,
not at the markup. Judge only what the record actually describes:
placement, prominence, hierarchy, density and visual tone.
[REPORT FORMAT]
### First Five Seconds
What a first-time visitor sees and understands before scrolling, in
three sentences.
### Layout And Hierarchy
How the page is built up, where the eye is led, whether the visual
weight matches the importance of the content.
### Calls To Action
Markdown table: Label | Where it sits | How prominent | What it seems to
promise.
### Navigation And Orientation
Menus, breadcrumbs, section markers, sticky elements, and whether a
visitor can tell where they are.
### Friction And Interruptions
Cookie dialogs, paywalls, modals, banners, ads, empty states,
placeholders — anything that stands between the visitor and the content.
### Accessibility Signals
Only what is visible in the screenshots: small text, thin contrast,
icon-only controls without labels, colour used as the only distinction,
cramped touch targets. Say plainly that this is a visual check, not a
technical audit.
### Five Improvements
Ranked, each one sentence, each tied to something concrete on the page.
[EVIDENCE]:
{{citationRule}}{{evidence}}
[UI AUDIT]:
param: gpt
isolated: true
- label: AFTER UI AUDIT
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = rebuild'
label: PAGE REBUILD
type: group
steps:
- label: GENERATE PAGE REBUILD
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
Rebuild the page as a self-contained markdown document that a reader
could use instead of the page itself. This is a reconstruction, not a
summary: nothing is compressed, nothing is commented on.
[RULES]
- Keep the original reading order, from the first segment to the last.
- Keep the heading hierarchy: page headings become `#`, `##`, `###` in
the same nesting they had on screen.
- Keep the body text as text, lists as lists, tables as markdown tables,
code as fenced code blocks.
- Put every image, chart, diagram, icon-with-meaning and photograph at
the exact place it appeared, as a blockquote line starting with `> 🖼️`
followed by what it shows.
- Merge the overlap between consecutive frames silently: repeated
headers, footers and navigation appear once, at the top, and are not
repeated per segment.
- Do not write segment markers, do not mention screenshots, do not add a
preface or a conclusion.
- Mark gaps inline as `> ⚠️ [unreadable]` where the record marks them.
[EVIDENCE]:
{{citationRule}}{{evidence}}
[REBUILT PAGE]:
param: gpt
isolated: true
- label: AFTER PAGE REBUILD
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = links'
label: LINKS AND ACTIONS
type: group
steps:
- label: GENERATE LINKS AND ACTIONS
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{groundRule}}
List everything on this page that a visitor could click, tap, type into
or follow, grouped by the part of the page it sits in.
[REPORT FORMAT]
### Inventory
Markdown table: Label | Type | Section | What it appears to do.
Type is one of: link, button, tab, menu item, form field, dropdown,
checkbox, search, download, social, other.
### Primary Paths
The three routes the page most obviously wants a visitor to take, each
one sentence.
### Dead Ends And Doubts
Controls whose purpose is not clear from the screenshot, labels that
repeat with different targets, and anything the record marks as
unreadable.
Only list what the record actually contains. Never invent a URL: the
record holds labels, not addresses, so write the label and say where it
sits.
[EVIDENCE]:
{{citationRule}}{{evidence}}
[LINKS AND ACTIONS]:
param: gpt
isolated: true
- label: AFTER LINKS AND ACTIONS
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = quoting'
label: QUOTE EXTRACTION
type: group
steps:
- label: ASK QUOTE TARGET
type: ask
message: >-
Please ask a specific question so that I can quote something from the
frames:
param: quote
options: null
default: ''
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: GENERATE QUOTE EXTRACTION
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
[INSTRUCTIONS FOR VERBATIM CITATION]
1. Quote exclusively from the [EVIDENCE], which is a verbatim reading of
the frames.
2. Do not fabricate or invent any text content.
3. Reproduce each quote exactly as recorded, without modification.
4. Name the exact frame each quote came from, using the `#### Frame N`
heading it sits under, and the segment marker above it.
5. Quote complete units of meaning. Never cut a sentence or an argument
in half; quote everything that belongs together.
6. If nothing in the record matches, say so plainly instead of inventing
a quote.
[CITATION FORMAT]
Frame: `[FRAME NUMBER]` (segment `[SEGMENT NUMBER]`)
Quote:
```
[EXACT QUOTE]
```
Context: one sentence on where this sits on the page.
[YOUR QUESTION]: {{quote}}
[EVIDENCE]:
{{evidence}}
[CITATIONS]:
param: gpt
isolated: true
- label: AFTER QUOTE EXTRACTION
type: jump
to: VERIFY OUTPUT
- condition: '{{format}} = record'
label: SHOW THE RECORD
type: group
steps:
- label: PRINT RECORD HEADER
type: say
message: >-
📜 The frame-by-frame reading, {{shotCount}} frames in
{{record.segments}} segment(s). This costs no request — it is already on
disk.
- label: PRINT RECORD
type: say
message: '{{payload}}'
- label: AFTER RECORD
type: jump
to: FOLLOW UP MENU
- condition: '{{format-option}} = $custom'
label: CUSTOM INSTRUCTION
type: group
steps:
- label: APPLY CUSTOM INSTRUCTION
type: gpt
prompt: >-
Please ignore all previous instructions. Write your entire answer in
{{outputLanguage}}.
{{promptHeader}}
{{buildRule}}
Whatever stands under [YOUR REQUEST] is what you have to do with the
record.
[RULES]
- Do what the request says and output the result alone: no preamble, no
account of what you did, no closing remark. Where it asks for code or a
document, the answer is that code or that document and nothing else.
- Never reply that the record "does not contain" what was asked for. The
record contains the page; what was asked for is what you build out of
it. A request to build something can always be carried out, because the
material is already there.
- Take every word, number, heading, label, price, name and image
description from the record and reproduce it exactly.
- Use every piece of visual form the record gives you - typefaces,
sizes, colours, spacing, alignment, layout, order. Where the record is
silent, choose something consistent with what it does say and carry on;
do not stop to ask and do not leave a placeholder for a decision you can
make yourself.
- Where the record marks a region `[unreadable]` or `NOT READ`, put a
clearly marked placeholder at that spot and continue. A gap never stops
the work.
- If the request is a question rather than a task, answer it from the
record and name the frame it came from.
[YOUR REQUEST]:
{{format}}
[EVIDENCE]:
{{citationRule}}{{evidence}}
[ANSWER]:
param: gpt
isolated: true
- label: AFTER CUSTOM INSTRUCTION
type: jump
to: VERIFY OUTPUT
- condition:
- '{{format}} = export'
- '{{change}} = export'
label: EXPORT ARTIFACTS
type: group
steps:
- label: ASK EXPORT FORMAT
type: ask
message: Which artifact should be written to disk?
param: exportFormat
options:
- label: 📝 REPORT · the current answer as Markdown
value: md
- label: 🖼️ RECORD · the full frame-by-frame reading
value: transcript
- label: 🧩 JSON · answer and record combined
value: json
- label: 📃 PLAIN · everything as plain text
value: txt
- label: 📋 CLIPBOARD · copy the answer instead of downloading
value: clipboard
default: md
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: WRITE EXPORT FILE
type: js
args: exportFormat, title, url, gpt, notes, shotCount, segment
code: >-
// FULL PAGE VISION · EXPORT
// The native EXPORT step is unreliable, so everything is built here,
and it is built
// as a ladder rather than a single attempt:
// 1. write a Blob and click a hidden anchor - the normal path;
// 2. if the page forbids that, open the content in a new tab so it
can still be
// saved by hand;
// 3. if that is blocked too, copy the content to the clipboard;
// 4. if even that fails, report honestly instead of pretending
success.
// The clipboard format skips straight to step three by design.
function s(v) {
// The record carries hardened braces so the engine never executes text lifted off a page.
// A file on disk is never interpolated, so the armour comes off again here and the export
// shows exactly what stood on the page.
return String(v == null ? '' : v).replace(/\{\{\\/g, '{{');
}
function pad(v) { return String(v).padStart(2, '0'); }
async function toClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) { /* fall through to the legacy path */ }
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
const done = document.execCommand('copy');
document.body.removeChild(ta);
return done === true;
} catch (e) {
return false;
}
}
const format = s(args.exportFormat).toLowerCase() || 'md';
const title = s(args.title).trim();
const url = s(args.url).trim();
const summary = s(args.gpt).trim();
const transcript = s(args.notes).trim();
const stamp = new Date();
const slug = (title || 'full-page-vision')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 60) || 'full-page-vision';
const stampStr = stamp.getFullYear() + pad(stamp.getMonth() + 1) +
pad(stamp.getDate()) +
'-' + pad(stamp.getHours()) + pad(stamp.getMinutes());
const isoDay = stamp.getFullYear() + '-' + pad(stamp.getMonth() + 1) +
'-' + pad(stamp.getDate());
// Front matter makes the file land in Obsidian, Logseq or Notion as a
real note
// instead of as a loose text blob.
const frontMatter = [
'---',
'title: "' + title.replace(/"/g, '\\"') + '"',
'source: ' + url,
'captured: ' + isoDay,
'frames: ' + s(args.shotCount),
'segments: ' + s(args.segment),
'reader: full-page-vision',
'---',
''
].join('\n');
let content = '';
let mime = 'text/markdown;charset=utf-8';
let ext = 'md';
if (format === 'json') {
mime = 'application/json;charset=utf-8';
ext = 'json';
content = JSON.stringify({
title: title,
url: url,
generatedAt: stamp.toISOString(),
frames: Number(s(args.shotCount).replace(/[^0-9]/g, '')) || 0,
segments: Number(s(args.segment).replace(/[^0-9]/g, '')) || 0,
answer: summary,
visionRecord: transcript
}, null, 2);
} else if (format === 'txt') {
mime = 'text/plain;charset=utf-8';
ext = 'txt';
content = [title, url, isoDay, '', summary, '', transcript].join('\n');
} else if (format === 'transcript') {
content = frontMatter + ['# ' + (title || 'Vision record'), '', summary ? '' : '', '---', '', transcript].join('\n');
} else if (format === 'clipboard') {
content = summary || transcript;
} else {
content = frontMatter + ['# ' + (title || 'Full page summary'), '', summary || transcript, ''].join('\n');
}
const filename = slug + '-' + stampStr + '.' + ext;
const result = { ok: 'no', filename: filename, bytes: content.length,
format: ext, via: 'none', message: '' };
if (format === 'clipboard') {
const copied = await toClipboard(content);
result.ok = copied ? 'yes' : 'no';
result.via = 'clipboard';
result.message = copied
? 'Copied to the clipboard (' + content.length + ' characters).'
: 'The clipboard was refused by this page. Try the Markdown download instead.';
return result;
}
// Step one: the download.
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(function () {
try { document.body.removeChild(a); } catch (e) { /* ignore */ }
try { URL.revokeObjectURL(href); } catch (e) { /* ignore */ }
}, 1500);
result.ok = 'yes';
result.via = 'download';
result.bytes = blob.size;
result.message = 'Saved as ' + filename + ' (' + blob.size + ' bytes).';
return result;
} catch (e) {
result.error = String((e && e.message) || e);
}
// Step two: a new tab the user can save by hand.
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const win = window.open(href, '_blank');
if (win) {
result.ok = 'yes';
result.via = 'tab';
result.message = 'The download was blocked, so ' + filename + ' was opened in a new tab. Save it from there.';
return result;
}
} catch (e) { /* fall through */ }
// Step three: the clipboard.
const copied = await toClipboard(content);
result.ok = copied ? 'yes' : 'no';
result.via = copied ? 'clipboard' : 'none';
result.message = copied
? 'Downloads are blocked on this page, so the content was copied to the clipboard instead.'
: 'This page blocks downloads, new tabs and the clipboard. Choose EXPORT again on a normal page.';
return result;
param: exportResult
timeout: 60000
onFailure: ''
silent: true
- condition: '{{exportResult.ok}} = yes'
label: CONFIRM EXPORT
type: say
message: 📦 {{exportResult.message}}
- condition: '{{exportResult.ok}} = no'
label: REPORT EXPORT FAILURE
type: say
message: ⚠️ {{exportResult.message}}
- label: AFTER EXPORT
type: jump
to: FOLLOW UP MENU
- condition:
- '{{format}} = repurpose'
- '{{change}} = repurpose'
label: REPURPOSE HANDOFF
type: group
steps:
- label: STAGE REPURPOSE INPUT
type: js
args: gpt, payload
code: >-
// FULL PAGE VISION · STAGE A HANDOVER
// Picks what to hand to the other command - the current answer if there
is one, the record
// otherwise - and armours it for exactly one interpolation hop.
//
// This used to be a CALC step, `handoff = '{{gpt}}'`, with a second
CALC falling back to
// `{{payload}}`. Both of those are interpolations, and the engine
consumes one layer of the
// `{{\` escape per interpolation. So the staged text arrived stripped,
and the very next hop
// - `inputs: ['{{handoff}}']` - interpolated it again, this time with
nothing protecting it:
// a `{{page}}` or `{{serp x}}` transcribed off the page went live on
the way into the other
// command. Selecting the source in JS means no interpolation happens
here at all, and the one
// layer added at the end is spent by the handover itself, so the
receiving command is given
// the literal text that stood on the page.
function strip(value) {
return String(value == null ? '' : value).replace(/\{\{\\+/g, '{{');
}
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const answer = strip(args.gpt).trim();
const record = strip(args.payload).trim();
const text = answer !== '' ? answer : record;
if (text === '') return '';
return harden(text);
param: handoff
timeout: 30000
onFailure: ''
silent: true
- label: CALL REPURPOSE COMMAND
type: command
name: Repurpose text
inputs:
- '{{handoff}}'
silent: false
- label: AFTER REPURPOSE
type: jump
to: FOLLOW UP MENU
- label: VERIFY OUTPUT
type: group
steps:
- label: HARDEN ANSWER
type: js
args: gpt
code: >-
// FULL PAGE VISION · HARDEN TEXT
// The one piece of armour this command cannot do without.
//
// The frames are read verbatim, so whatever stands on the page ends up
inside a parameter -
// and on a page that documents a template language, that includes
things like {{page}},
// {{serp query}} or {{transcript}}. The moment such a record is
interpolated into the next
// prompt, those look exactly like real system parameters: the engine
goes off and fetches a
// whole page, runs a web search or pulls a transcript, once per
occurrence, for every answer
// that carries the record. A documentation page with forty examples in
it turns a two second
// step into minutes of work that nobody asked for. It looks like a
hang; it is a record
// quoting the engine's own syntax back at it.
//
// The cure is the escape the engine itself defines: a backslash after
the opening braces
// marks them as literal. The text still reads as {{page}} for a human
and for the model, it
// simply stops being a command. Idempotent, so text that has already
been hardened is left
// alone rather than growing a second backslash on every pass.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
// The step is used on several different slots, so it accepts whichever
one it was given.
const candidates = ['payload', 'gpt', 'notes', 'text', 'condensed',
'segmentNotes'];
let raw = '';
for (let i = 0; i < candidates.length; i++) {
if (args[candidates[i]] != null && String(args[candidates[i]]) !== '') {
raw = args[candidates[i]];
break;
}
}
return harden(raw);
param: gpt
timeout: 30000
onFailure: ''
silent: true
- condition: '{{method}} = retrieval'
label: VERIFY THE FRAME CITATIONS
type: js
args: gpt, evidence
code: >-
// FULL PAGE VISION · CITATION REPAIR
// Runs after an answer that was written from a selection rather than
from the whole record.
// It takes every quote the answer put in its Source References block,
finds where that text
// really sits in the evidence, and rewrites the citation to point at
the passage it actually
// came from. Quotes it cannot find anywhere are removed and named.
//
// The reason for repairing rather than merely checking: a model that is
told its quotes will
// be located and corrected has no incentive to approximate, and a
citation that survives this
// step is one a reader can act on - scroll to that frame, look at that
screen, see the thing.
// A citation that is only asserted is worth nothing.
//
// Two stages, and the separation is the whole design.
//
// LOCATE works on characters, and is allowed to be generous. Its only
job is to find the
// passage; a stray digit or a collapsed line break must not hide text
that is really there.
//
// JUDGE works on ordered content tokens, and is not generous at all.
Character similarity
// cannot tell "the button is disabled" from "the button is not
disabled" - two characters
// apart, opposite meanings. Ordered tokens can, and the numeric,
negation and modal guards
// below catch the rest: swapping a number, dropping a "not", or
turning "may" into "must"
// all fail even when every other word matches.
function norm(s) {
return String(s == null ? '' : s)
.normalize('NFKC')
.replace(/[\u200B-\u200F\u00AD\uFEFF]/g, '')
.replace(/[\u2018\u2019\u201C\u201D\u00AB\u00BB\u201E]/g, '"')
.replace(/[\u2010-\u2015\u2212]/g, '-')
.replace(/\s+/g, ' ')
.toLowerCase()
.trim();
}
function lev(a, b) {
const m = a.length, n = b.length;
if (!m) return n;
if (!n) return m;
let prev = new Array(n + 1), cur = new Array(n + 1);
for (let j = 0; j <= n; j++) prev[j] = j;
for (let i = 1; i <= m; i++) {
cur[0] = i;
for (let j = 1; j <= n; j++) {
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1,
prev[j - 1] + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1));
}
const t = prev; prev = cur; cur = t;
}
return prev[n];
}
function lcs(a, b) {
const m = a.length, n = b.length;
if (!m || !n) return 0;
let prev = new Array(n + 1).fill(0), cur = new Array(n + 1).fill(0);
for (let i = 1; i <= m; i++) {
cur[0] = 0;
for (let j = 1; j <= n; j++) {
cur[j] = a[i - 1] === b[j - 1] ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]);
}
const t = prev; prev = cur; cur = t;
}
return prev[n];
}
const STOP = { the: 1, a: 1, an: 1, of: 1, to: 1, in: 1, and: 1, or: 1,
is: 1, are: 1, be: 1,
as: 1, that: 1, this: 1, it: 1, for: 1, on: 1, with: 1, by: 1, at: 1, from: 1, was: 1,
were: 1, der: 1, die: 1, das: 1, und: 1, ist: 1, von: 1, zu: 1, den: 1, dem: 1, ein: 1,
eine: 1, im: 1, auf: 1, mit: 1, als: 1 };
const NEG =
/^(?:not|no|never|none|nor|without|neither|cannot|nicht|kein|keine|keiner|nie|niemals|ohne|weder)$/;
const CRIT =
/^(?:must|shall|should|may|might|can|could|will|would|always|never|all|every|any|none|some|most|few|only|more|less|higher|lower|larger|smaller|enabled|disabled|active|inactive|muss|kann|soll|darf|immer|nie|alle|jeder|einige|nur|mehr|weniger)$/;
function tokens(s) {
return norm(s).split(/[^\p{L}\p{N}\p{M}]+/u).filter(Boolean);
}
function content(s) {
return tokens(s).filter(function (w) { return !STOP[w]; });
}
// A character map back into the original text, so a located quote can
be restored to the
// exact wording the record carries rather than to the model's rendering
of it.
function mapped(src) {
const raw = String(src == null ? '' : src);
let out = '';
const map = [];
for (let i = 0; i < raw.length; i++) {
let c = raw.charAt(i);
if (/\s/.test(c)) {
if (out.charAt(out.length - 1) === ' ') continue;
c = ' ';
} else {
c = norm(c);
if (!c) continue;
}
out += c;
map.push(i);
}
return { text: out, map: map };
}
const answer = String(args.gpt == null ? '' : args.gpt);
const evidence = String(args.evidence == null ? '' : args.evidence);
if (answer.trim() === '' || evidence.trim() === '') {
return { answer: answer, audit: '', total: 0, verified: 0, ok: 'no' };
}
// The passage boundaries, so a located offset can be turned back into a
passage number, and
// the map that binds each number to a frame.
const marks = [];
evidence.replace(/\[(\d+)\]\s/g, function (m, n, off) {
marks.push({ at: off, n: parseInt(n, 10) });
return m;
});
const stripped = evidence.replace(/\[(\d+)\]\s/g, function (m) {
return new Array(m.length + 1).join(' ');
});
function passageAt(off) {
let p = 0;
for (let i = 0; i < marks.length; i++) {
if (marks[i].at <= off) p = marks[i].n; else break;
}
return p;
}
const FRAME = {};
String(evidence).split('\n').slice(0, 400).forEach(function (line) {
const m = line.match(/^\[(\d+)\]\s*=\s*(.+)$/);
if (m) FRAME[m[1]] = m[2].trim();
});
const H = mapped(stripped);
function locate(quote) {
const q = norm(quote);
if (q.length < 12) return null;
const at = H.text.indexOf(q);
if (at > -1) return { at: at, len: q.length, sc: 1 };
const w = q.split(' ').filter(Boolean);
const mid = w.length >> 1;
const anchors = [];
[w.slice(0, 4).join(' '), w.slice(-4).join(' '), w.slice(mid, mid + 4).join(' ')]
.forEach(function (a) { if (a.length >= 6 && anchors.indexOf(a) === -1) anchors.push(a); });
let best = null, tries = 0;
const factors = [0.9, 1.0, 1.15, 1.35];
for (let ai = 0; ai < anchors.length && tries < 600; ai++) {
let i = -1;
while ((i = H.text.indexOf(anchors[ai], i + 1)) !== -1 && tries < 600) {
const shifts = [0, -(q.length >> 2), -(q.length >> 1)];
for (let si = 0; si < shifts.length; si++) {
const st = Math.max(0, i + shifts[si]);
for (let fi = 0; fi < factors.length; fi++) {
const seg = H.text.substr(st, Math.round(q.length * factors[fi]));
if (seg.length < 8) continue;
tries++;
const sc = 1 - lev(q, seg) / Math.max(q.length, seg.length);
if (!best || sc > best.sc) best = { at: st, len: seg.length, sc: sc };
}
}
}
}
return best && best.sc >= 0.55 ? best : null;
}
function judge(quote, window) {
const q = norm(quote), w = norm(window);
const charSim = 1 - lev(q, w) / Math.max(q.length, w.length);
const cq = content(quote), cw = content(window);
if (!cq.length) return 'unsupported';
const order = lcs(cq, cw) / cq.length;
const bag = {};
let hits = 0;
cw.forEach(function (t) { bag[t] = (bag[t] || 0) + 1; });
cq.forEach(function (t) { if (bag[t]) { bag[t]--; hits++; } });
const coverage = hits / cq.length;
// Same words in a different order is the signature of an inverted claim. For a real quote
// the two measures move together, so only genuine reordering separates them.
const dice = (2 * hits) / (cq.length + cw.length);
const ordDice = (2 * lcs(cq, cw)) / (cq.length + cw.length);
if (dice - ordDice > 0.10) return 'unsupported';
const dq = q.match(/\d+(?:[.,]\d+)?/g) || [];
const dw = w.match(/\d+(?:[.,]\d+)?/g) || [];
if (!dq.every(function (x) { return dw.indexOf(x) !== -1; })) return 'unsupported';
const tq = tokens(q), tw = tokens(w);
const have = {};
tw.forEach(function (x) { have[x] = 1; });
if (!tq.filter(function (x) { return CRIT.test(x); })
.every(function (x) { return have[x]; })) return 'unsupported';
if (tq.filter(function (x) { return NEG.test(x); }).length !==
tw.filter(function (x) { return NEG.test(x); }).length) return 'unsupported';
if (charSim >= 0.97 && order >= 0.97) return 'verbatim';
if (cq.length >= 3 && order >= 0.80 && coverage >= 0.85) return 'near';
return 'unsupported';
}
// `[1]` is the citation form. The optional tail matches the older `[1 ·
Frame 3]` shape so
// that a model which produces it anyway is repaired into the plain form
rather than left
// alone: one scheme in the answer, always.
const CITE = /\[\s*(\d+)\s*(?:[·:\-\u2013][^\]]*)?\]/g;
const WRAP =
/^[\s"'\u00AB\u00BB\u201C\u201D\u201E\u2018\u2019]+|[\s"'\u00AB\u00BB\u201C\u201D\u201E\u2018\u2019]+$/g;
const lines = answer.split(/\r?\n/);
const keep = [];
let total = 0, exact = 0, near = 0, recited = 0;
const dropped = [];
const good = {}, bad = {};
for (let li = 0; li < lines.length; li++) {
const line = lines[li];
if (!/^\s*>/.test(line)) { keep.push(line); continue; }
const body = line.replace(/^\s*>\s?/, '');
const ids = [];
let m;
CITE.lastIndex = 0;
while ((m = CITE.exec(body)) !== null) ids.push(m[1]);
const bare = body.replace(CITE, '').replace(/\*\*/g, '')
.replace(/\[[^\]]*\]/g, '').replace(WRAP, '').trim();
if (bare.length < 12) { keep.push(line); continue; }
total++;
const hit = locate(bare);
const tier = hit ? judge(bare, H.text.substr(hit.at, hit.len)) : 'unsupported';
if (tier === 'unsupported') {
ids.forEach(function (id) { bad[id] = 1; });
dropped.push(bare.length > 88 ? bare.slice(0, 88) + '…' : bare);
continue;
}
const s0 = H.map[hit.at];
const e0 = H.map[Math.min(hit.at + hit.len - 1, H.map.length - 1)];
let original = stripped.slice(s0, e0 + 1).replace(/\s+/g, ' ').replace(WRAP, '').trim();
if (!original) original = bare;
// The citation the reader sees is the passage number and nothing else. The frame stays on
// the evidence side, where it grounds the model and lets this step repair a wrong number;
// it is not repeated in the answer, where it would only be noise beside the number that
// already identifies the passage.
const trueId = String(passageAt(s0));
const cite = '[' + trueId + ']';
if (ids.length && ids[0] !== trueId) recited++;
ids.forEach(function (id) { good[id] = 1; });
good[trueId] = 1;
if (tier === 'verbatim') { exact++; keep.push('> "' + original + '" **' + cite + '**'); }
else { near++; keep.push('> "' + original + '" **' + cite + '** 〰️'); }
}
let clean = keep.join('\n').replace(/\n{3,}/g, '\n\n').trim();
let marked = 0;
if (total > 0) {
clean = clean.replace(CITE, function (full, id) {
if (bad[id] && !good[id]) { marked++; return '[⚠️ unverified]'; }
return full;
});
}
clean = clean.replace(/\s+([.,;:!?])/g, '$1').replace(/[ \t]{2,}/g, '
').trim();
const verified = exact + near;
let audit;
if (total === 0) {
audit = '';
} else {
const bits = [exact + ' verbatim'];
if (near) bits.push(near + ' near-verbatim 〰️');
if (dropped.length) bits.push(dropped.length + ' unsupported');
audit = '🛡️ **Citation audit** · ' + verified + '/' + total + ' verified · ' + bits.join(' · ');
if (recited) {
audit += '\n> 🔢 ' + recited + ' citation number' + (recited === 1 ? '' : 's') +
' corrected to the passage the quote really sits in';
}
const mapped = Object.keys(FRAME).length;
if (mapped) audit += '\n> 🗺️ ' + mapped + ' passage(s) in the evidence map';
if (marked) {
audit += '\n> ⚠️ ' + marked + ' claim' + (marked === 1 ? '' : 's') +
' left without support — marked inline';
}
dropped.forEach(function (d) { audit += '\n> ❌ not found in the evidence: ' + d; });
}
// The record is armoured; the audit quotes it back into a chat message,
so it is armoured too.
audit = audit.replace(/\{\{(?!\\)/g, '{{\\');
return { answer: clean, audit: audit, total: total, verified: verified,
ok: total > 0 ? 'yes' : 'no' };
param: citationCheck
timeout: 60000
onFailure: ''
silent: true
- condition: '{{citationCheck.ok}} = yes'
label: ADOPT THE REPAIRED ANSWER
type: calc
func: set
param: gpt
value: '{{citationCheck.answer}}'
format: ''
- condition: '{{citationCheck.ok}} = yes'
label: REARMOUR THE REPAIRED ANSWER
type: js
args: gpt
code: >-
// FULL PAGE VISION · RE-ARMOUR AN ANSWER
// One layer of the `{{\` escape, applied idempotently. Used wherever an
answer has just
// passed through a CALC value, which the engine defines as consuming
exactly one layer.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
return harden(args.gpt);
param: gpt
timeout: 60000
onFailure: ''
silent: true
- condition: '{{citationCheck.ok}} = yes'
label: SHOW THE CITATION AUDIT
type: say
message: '{{citationCheck.audit}}'
- label: CHECK ANSWER
type: js
args: gpt
code: >-
// FULL PAGE VISION · ANSWER CHECK
// Every generated answer passes through here before the follow up menu
is offered.
// An empty or stub answer is not treated as a result, so the menu can
offer a real
// retry instead of letting the run end on a blank message.
const raw = String(args.gpt == null ? '' : args.gpt).trim();
const chars = raw.length;
return {
ok: chars > 20 ? 'yes' : 'no',
chars: chars,
words: chars > 0 ? raw.split(/\s+/).length : 0
};
param: answerCheck
timeout: 30000
onFailure: ''
silent: true
- condition: '{{answerCheck.ok}} = no'
label: WARN EMPTY ANSWER
type: say
message: >-
⚠️ The model returned nothing usable for this output. The record is
still loaded — pick **RETURN BACK** below and choose an output again, or
type what you want instead. No frame is captured a second time.
- label: FOLLOW UP MENU
type: ask
message: >-
Anything else for this page? Pick one, or type another instruction or
question — the record stays loaded, so nothing is captured again.
param: change
options:
- label: ⬇️ SHORTEN · keep only the essentials
value: shorten
- label: 💬 SIMPLIFY · plainer language
value: simplify
- label: 🔍 CLARIFY · sharper and less ambiguous
value: clarify
- label: 📐 EXPAND · more depth and detail
value: expand
- label: 🌍 TRANSLATE · the same result in another language
value: translate
- label: 🕵️ FACT CHECK · hand over to the Fact check command
value: factCheck
- label: 📦 EXPORT · download or copy the result
value: export
- label: ♻️ REPURPOSE · hand over to the Repurpose text command
value: repurpose
- label: 🔙 RETURN BACK · another output from the same record
value: return
- value: $custom
- label: 🔄 READING METHOD · switch between the full record and retrieval
value: method
default: return
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- condition: '{{change}} = translate'
label: TRANSLATE RESULT
type: group
steps:
- label: ASK TARGET LANGUAGE
type: ask
message: Into which language should the current result be translated?
param: targetLanguage
options: null
default: English
vision:
enabled: false
mode: area
hint: ''
send: true
optionsInvalid: false
- label: TRANSLATE THE ANSWER
type: gpt
prompt: >-
Please ignore all previous instructions.
Translate the [TEXT] into {{targetLanguage}}. Output nothing but the
translation: no preamble, no quotes around it, no commentary. Keep the
markdown structure, the headings, the tables and the code blocks exactly
as they are. Keep numbers, units, dates, proper nouns and quoted
passages faithful; translate the surrounding prose.
[TEXT]:
{{gpt}}
[TRANSLATION]:
param: gpt
isolated: true
- label: AFTER TRANSLATION
type: jump
to: VERIFY OUTPUT
- condition: '{{change}} = export'
label: ROUTE FOLLOW UP EXPORT
type: jump
to: EXPORT ARTIFACTS
- condition: '{{change}} = repurpose'
label: ROUTE FOLLOW UP REPURPOSE
type: jump
to: REPURPOSE HANDOFF
- condition: '{{change}} = return'
label: ROUTE BACK TO OUTPUT MENU
type: jump
to: SELECT OUTPUT
- condition:
- '{{change}} = shorten'
- '{{change}} = simplify'
- '{{change}} = clarify'
- '{{change}} = expand'
label: REVISE RESULT
type: group
steps:
- condition: '{{change}} = shorten'
label: INSTRUCTION SHORTEN
type: calc
func: set
param: changeInstruction
value: >-
Make it shorter, keeping only the most essential points. Drop repetition
and padding, never drop a fact that carries weight.
format: ''
- condition: '{{change}} = simplify'
label: INSTRUCTION SIMPLIFY
type: calc
func: set
param: changeInstruction
value: >-
Simplify the text and rewrite it in more accessible language. Explain
the terms that need explaining, keep the substance intact.
format: ''
- condition: '{{change}} = clarify'
label: INSTRUCTION CLARIFY
type: calc
func: set
param: changeInstruction
value: >-
Add clarity and conciseness. The text should be unambiguous, well
ordered and easy to follow.
format: ''
- condition: '{{change}} = expand'
label: INSTRUCTION EXPAND
type: calc
func: set
param: changeInstruction
value: >-
Expand it. Add the depth, detail, figures and context that the record
contains but the current answer left out.
format: ''
- label: APPLY REVISION
type: gpt
prompt: >-
Please ignore all previous instructions. Write only in
{{outputLanguage}}.
Revise the [TEXT TO CHANGE] according to the [CHANGES TO MAKE]. Output
nothing but the revised reply: do not echo this command, do not put
quotes around the result, do not add commentary. Stay grounded in the
[EVIDENCE] and never add facts it does not contain.
[CHANGES TO MAKE]:
{{changeInstruction}}
[TEXT TO CHANGE]:
{{gpt}}
[EVIDENCE]:
{{citationRule}}{{evidence}}
[REVISED REPLY]:
param: gpt
isolated: true
- label: AFTER REVISION
type: jump
to: VERIFY OUTPUT
- condition: '{{change}} = factCheck'
label: FACT CHECK HANDOFF
type: group
steps:
- label: STAGE FACT CHECK INPUT
type: js
args: gpt, payload
code: >-
// FULL PAGE VISION · STAGE A HANDOVER
// Picks what to hand to the other command - the current answer if there
is one, the record
// otherwise - and armours it for exactly one interpolation hop.
//
// This used to be a CALC step, `handoff = '{{gpt}}'`, with a second
CALC falling back to
// `{{payload}}`. Both of those are interpolations, and the engine
consumes one layer of the
// `{{\` escape per interpolation. So the staged text arrived stripped,
and the very next hop
// - `inputs: ['{{handoff}}']` - interpolated it again, this time with
nothing protecting it:
// a `{{page}}` or `{{serp x}}` transcribed off the page went live on
the way into the other
// command. Selecting the source in JS means no interpolation happens
here at all, and the one
// layer added at the end is spent by the handover itself, so the
receiving command is given
// the literal text that stood on the page.
function strip(value) {
return String(value == null ? '' : value).replace(/\{\{\\+/g, '{{');
}
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
const answer = strip(args.gpt).trim();
const record = strip(args.payload).trim();
const text = answer !== '' ? answer : record;
if (text === '') return '';
return harden(text);
param: handoff
timeout: 30000
onFailure: ''
silent: true
- label: CALL FACT CHECK COMMAND
type: command
name: Fact check
inputs:
- '{{handoff}}'
silent: false
- label: AFTER FACT CHECK
type: jump
to: FOLLOW UP MENU
- condition: '{{change-option}} = $custom'
label: CUSTOM FOLLOW UP
type: group
steps:
- condition: '{{method}} = retrieval'
label: PREPARE THE EVIDENCE FOR THE FOLLOW UP
type: group
steps:
- label: BUILD THE FOLLOW UP QUERY
type: js
args: format, focus, change
code: >-
// FULL PAGE VISION · WHAT THIS REQUEST IS LOOKING FOR
// Turns the request at hand into something the index can be
searched with. Three sources, in
// order of precedence: a follow-up the user just typed, the
instruction given before the
// reading, and otherwise the output mode that was picked from the
menu.
//
// The output modes are not questions, so they cannot be searched
for as if they were. Each
// one is described by the record labels it lives in and by the
words the record uses inside
// those labels - `**Visual:**` for a visual analysis, `**Table:**`
and `**Data:**` for a data
// extraction - and the ones that are inherently about the whole
page are marked `spread`,
// which makes the selector sample evenly across every frame instead
of piling up wherever the
// keywords happen to cluster. A summary built from the top of a
ranking is not a summary.
function clean(v) {
return String(v == null ? '' : v).replace(/\s+/g, ' ').trim();
}
const MODES = {
quick: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
full: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
structured: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
rebuild: { intent: 'SUMMARY', spread: true, labels: [], text: '' },
quoting: { intent: 'SUMMARY', spread: true, labels: ['Text', 'Heading'], text: '' },
visuals: { intent: 'TARGET', spread: false, labels: ['Visual'],
text: 'image illustration photograph icon logo avatar chart diagram map screenshot figure colour layout typeface' },
data: { intent: 'TARGET', spread: false, labels: ['Table', 'Data'],
text: 'number value price date percent currency unit table row column axis series total amount rating count' },
audit: { intent: 'TARGET', spread: false, labels: ['UI'],
text: 'button link tab menu field form input checkbox dropdown search navigation label placeholder state disabled' },
links: { intent: 'TARGET', spread: false, labels: ['UI'],
text: 'link button action navigation download tab menu href anchor call to action' }
};
// The values the two menus can put into these slots. A menu value
is a routing token, not
// something to search the record for: retrieving the passages that
contain the word "shorten"
// would answer a question nobody asked.
const MENU = {
shorten: 1, simplify: 1, clarify: 1, expand: 1, translate: 1, factCheck: 1,
export: 1, repurpose: 1, return: 1, retry: 1, no: 1, record: 1, focus: 1
};
const followUp = clean(args.change);
const mode = clean(args.format).toLowerCase();
// A follow-up the user just typed outranks everything: it is the
newest thing they said.
if (followUp !== '' &&
!Object.prototype.hasOwnProperty.call(MENU, followUp) &&
!Object.prototype.hasOwnProperty.call(MODES, followUp)) {
return { text: followUp, intent: 'QUERY', spread: false, labels: [], skip: false,
source: 'follow-up' };
}
// The instruction given before the reading, when the focused answer
is what is being produced.
let request = '';
try {
const f = args.focus;
if (f && typeof f === 'object' && f.request) request = clean(f.request);
} catch (e) { request = ''; }
if (mode === 'focus' && request !== '') {
return { text: request, intent: 'QUERY', spread: false, labels: [], skip: false,
source: 'instruction' };
}
// Two outputs never read the evidence at all: SHOW THE RECORD
prints the whole record and
// the export writes it to a file. Running a selection for them
would burn a BM25 pass over
// every passage to produce something nobody looks at.
if (mode === 'record' || mode === 'export') {
return { text: '', intent: 'NONE', spread: true, labels: [], skip: true,
source: 'not needed' };
}
const preset = Object.prototype.hasOwnProperty.call(MODES, mode) ?
MODES[mode] : null;
if (preset) {
// The instruction from before the reading still colours a preset output, because it is
// what the record was written to serve.
const text = (preset.text + ' ' + request).trim();
return { text: text, intent: preset.intent, spread: preset.spread,
labels: preset.labels, skip: false, source: 'output mode' };
}
// Anything else in the output menu is a free instruction typed by
the user.
if (mode !== '') {
return { text: clean(args.format), intent: 'QUERY', spread: false, labels: [],
skip: false, source: 'instruction' };
}
return { text: request, intent: 'SUMMARY', spread: true, labels: [],
skip: false,
source: 'whole record' };
param: query
timeout: 30000
onFailure: ''
silent: true
- label: SELECT THE EVIDENCE FOR THE FOLLOW UP
type: js
args: passages, query, payload
code: >-
// FULL PAGE VISION · EVIDENCE SELECTION
// Picks the passages this one request needs and lays them out with
a map that binds every
// passage number to the frame it came from.
//
// The scoring is BM25 over the passage index, run once per keyword
set and fused by
// reciprocal rank, which is the standard way to combine several
rankings without having to
// make their scores comparable. Three sets are used:
//
// A · the request's own words.
// B · dictionary-free prefix stems of them, so an inflected
language still matches -
// "Schriftarten" in the request finds "Schriftart" in the
record without anybody
// shipping a stemmer.
// C · the words the record itself uses around the first-pass
hits. This is the classic
// pseudo-relevance feedback step: take the strongest passages
from pass one, find the
// terms that are common inside them and rare everywhere else,
and search again with
// those. It is the same job the source command gives to a
model - "keywords that would
// appear inside the ANSWER passage itself, phrased the way
this corpus phrases things" -
// except that here the corpus is asked instead of guessed,
which costs nothing, cannot
// hallucinate a term, and is automatically in the record's
own language and wording.
//
// Three things make this different from a plain search:
//
// * LABEL BOOST. Our passages are not prose, they are labelled
bullets. A data extraction
// wants the passages carrying `**Table:**` and `**Data:**`, and
saying so directly beats
// hoping the word "table" happens to appear.
//
// * SPREAD. A summary, a report or a page rebuild is about the
whole page. Ranking cannot
// answer that question - the top of any ranking is wherever the
keywords cluster - so
// these sample evenly across the frames instead, from the first
to the last.
//
// * NEIGHBOURS. A bullet cut at a passage border finishes in the
next one. After the
// selection is made, a few immediate neighbours of the
strongest hits are pulled in, in
// reading order, so a `**Cut:**` never arrives without its
other half.
//
// The output is always in reading order, never in score order: the
record's own sequence is
// information, and a model handed frames 12, 3, 40, 7 has to
reconstruct it before it can use
// anything.
const BUDGET = 40000;
const RRF_K = 60;
const MAX_PICK = 60;
function norm(s) {
return String(s == null ? '' : s)
.normalize('NFKC')
.replace(/[\u2010-\u2015\u2212]/g, '-')
.replace(/[\u2018\u2019\u201C\u201D\u00AB\u00BB]/g, '"')
.replace(/\s+/g, ' ')
.toLowerCase()
.trim();
}
const CJK =
/[\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\u0E00-\u0E7F]/;
function words(s) {
const out = [];
const parts = norm(s).split(/[^\p{L}\p{N}\p{M}]+/u);
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (!p) continue;
if (CJK.test(p)) {
if (p.length <= 2) { out.push(p); continue; }
for (let j = 0; j + 2 <= p.length; j++) out.push(p.substr(j, 2));
} else if (p.length >= 3) {
out.push(p);
}
}
return out;
}
function uniq(list, cap) {
const seen = {}, out = [];
for (let i = 0; i < list.length; i++) {
const k = list[i];
if (!k || seen[k]) continue;
seen[k] = 1;
out.push(k);
if (cap && out.length >= cap) break;
}
return out;
}
// A hit only counts when the term starts at a word boundary.
Without that guard "art" scores
// on "start", "particular" and "Artefakte", and a request about
typefaces retrieves the
// sidebar.
const WORDCH = /[0-9\p{L}\p{M}]/u;
function tf(hay, kw) {
let n = 0, i = 0;
while ((i = hay.indexOf(kw, i)) !== -1) {
if (i === 0 || !WORDCH.test(hay.charAt(i - 1))) n++;
i += kw.length;
}
return n;
}
let index = { chunks: [], totalChunks: 0, totalChars: 0 };
try {
const p = args.passages;
if (p && typeof p === 'object' && p.chunks) index = p;
} catch (e) { /* fall through to the whole record */ }
const chunks = index.chunks || [];
const fallback = String(args.payload == null ? '' : args.payload);
if (!chunks.length) {
return { text: fallback, stats: '', picked: 0, total: 0, ok: 'no', mode: 'whole record' };
}
let q = { text: '', intent: 'SUMMARY', spread: true, labels: [] };
try {
const raw = args.query;
if (raw && typeof raw === 'object') q = raw;
} catch (e) { /* keep the default */ }
const labels = Array.isArray(q.labels) ? q.labels : [];
const spread = q.spread === true || q.spread === 'true';
// Some outputs do not read the evidence at all. Selecting for them
would be a full BM25 pass
// over every passage to produce something nobody looks at.
if (q.skip === true || q.skip === 'true') {
return { text: fallback, stats: '', picked: 0, total: 0, ok: 'no', mode: 'not needed',
views: 0, expanded: 0 };
}
const N = chunks.length;
const lower = chunks.map(function (c) { return norm(c.text); });
const lens = chunks.map(function (c) { return String(c.text).length;
});
let avgdl = 0;
for (let i = 0; i < N; i++) avgdl += lens[i];
avgdl = avgdl / Math.max(N, 1);
// Which passages carry the labels this output lives in.
const labelHit = chunks.map(function (c) {
if (!labels.length) return false;
for (let i = 0; i < labels.length; i++) {
if (String(c.text).indexOf('**' + labels[i] + ':**') !== -1) return true;
}
return false;
});
function rank(kws) {
const k1 = 1.5, b = 0.75, df = {};
kws.forEach(function (kw) {
let c = 0;
for (let i = 0; i < N; i++) if (lower[i].indexOf(kw) !== -1) c++;
df[kw] = c;
});
const scored = [];
for (let i = 0; i < N; i++) {
let sc = 0;
for (let j = 0; j < kws.length; j++) {
const t = tf(lower[i], kws[j]);
if (t <= 0) continue;
const idf = Math.log((N - df[kws[j]] + 0.5) / (df[kws[j]] + 0.5) + 1);
sc += idf * (t * (k1 + 1)) / (t + k1 * (1 - b + b * lens[i] / avgdl));
}
if (sc > 0) scored.push({ i: i, s: sc });
}
scored.sort(function (a, b2) { return b2.s - a.s; });
return scored;
}
const surface = uniq(words(q.text || ''), 24);
const stems = uniq(surface.map(function (w) {
if (w.length < 6) return '';
const cut = Math.max(5, Math.ceil(w.length * 0.75));
return cut < w.length ? w.slice(0, cut) : '';
}).filter(Boolean), 24).filter(function (x) { return
surface.indexOf(x) === -1; });
const sets = [];
if (surface.length) sets.push(surface);
if (stems.length) sets.push(stems);
const rrf = {};
let bestRaw = 0;
let views = 0;
let firstPass = [];
function fuse(kws) {
const ranked = rank(kws);
if (!ranked.length) return [];
if (ranked[0].s > bestRaw) bestRaw = ranked[0].s;
for (let r = 0; r < ranked.length && r < 80; r++) {
rrf[ranked[r].i] = (rrf[ranked[r].i] || 0) + 1 / (RRF_K + r + 1);
}
views++;
return ranked;
}
for (let si = 0; si < sets.length; si++) {
const ranked = fuse(sets[si]);
if (si === 0) firstPass = ranked;
}
// Set C · pseudo-relevance feedback. Only for a question somebody
actually typed: a canned
// word list for a preset output has nothing to expand, and
expanding a spread query would
// pull the sample towards whatever the first pass happened to like.
let expansion = [];
if (String(q.intent || '') === 'QUERY' && firstPass.length) {
const top = firstPass.slice(0, 5).map(function (x) { return x.i; });
const inTop = {};
top.forEach(function (i) {
const seen = {};
words(chunks[i].text).forEach(function (w) {
if (seen[w]) return;
seen[w] = 1;
inTop[w] = (inTop[w] || 0) + 1;
});
});
const already = {};
sets.forEach(function (set) { set.forEach(function (w) { already[w] = 1; }); });
const cand = [];
Object.keys(inTop).forEach(function (w) {
if (already[w] || w.length < 4 || inTop[w] < 2) return;
let df = 0;
for (let i = 0; i < N; i++) if (lower[i].indexOf(w) !== -1) df++;
if (df === 0 || df > N * 0.5) return;
cand.push({ w: w, s: inTop[w] * Math.log(N / df) });
});
cand.sort(function (a, b2) { return b2.s - a.s; });
expansion = cand.slice(0, 12).map(function (c) { return c.w; });
if (expansion.length) fuse(expansion);
}
// The label boost is additive and deliberately large enough to
outrank a weak lexical hit:
// for a labelled output the label IS the query, and the words are
only a tie-breaker.
if (labels.length) {
for (let i = 0; i < N; i++) {
if (labelHit[i]) rrf[i] = (rrf[i] || 0) + 1 / RRF_K;
}
}
let fused = Object.keys(rrf).map(function (k) {
return { i: parseInt(k, 10), s: rrf[k] };
});
fused.sort(function (a, b2) { return b2.s - a.s; });
let picked = [];
let mode;
if (spread || !fused.length) {
// Even coverage of the whole record, in reading order, as many as the budget allows.
// "no lexical match" is only honest when there were words to match with: a summary has no
// question behind it, so nothing failed - it was never a search in the first place.
mode = fused.length ? 'spread'
: (sets.length ? 'spread (no lexical match)' : 'spread (whole record)');
const step = Math.max(1, Math.floor(N / Math.max(1, Math.min(MAX_PICK, N))));
for (let i = 0; i < N && picked.length < MAX_PICK; i += step) picked.push(i);
// The first and the last frame anchor a summary: the page starts and ends somewhere.
if (picked.indexOf(0) === -1) picked.unshift(0);
if (picked.indexOf(N - 1) === -1) picked.push(N - 1);
// Strong lexical hits are added on top, because a spread is a floor and not a ceiling.
for (let f = 0; f < fused.length && f < 12; f++) {
if (picked.indexOf(fused[f].i) === -1) picked.push(fused[f].i);
}
} else {
mode = labels.length ? 'labelled + ranked' : 'ranked';
for (let f = 0; f < fused.length && picked.length < MAX_PICK; f++) picked.push(fused[f].i);
}
// Neighbour recovery for the strongest hits, so a bullet cut at a
border keeps its other half.
const chosen = {};
picked.forEach(function (i) { chosen[i] = 1; });
let grown = 0;
for (let p = 0; p < picked.length && grown < 8; p++) {
const i = picked[p];
if (i > 0 && !chosen[i - 1]) { chosen[i - 1] = 1; picked.push(i - 1); grown++; }
if (grown >= 8) break;
if (i + 1 < N && !chosen[i + 1]) { chosen[i + 1] = 1; picked.push(i + 1); grown++; }
}
// Budget in order of strength, then restore reading order.
const strength = {};
picked.forEach(function (i, r) { strength[i] = r; });
picked.sort(function (a, b2) { return strength[a] - strength[b2];
});
const selected = [];
let used = 0;
for (let p = 0; p < picked.length; p++) {
const i = picked[p];
if (used + lens[i] > BUDGET && selected.length > 0) continue;
selected.push(i);
used += lens[i];
}
selected.sort(function (a, b2) { return a - b2; });
if (!selected.length) {
return { text: fallback, stats: '', picked: 0, total: N, ok: 'no', mode: 'whole record' };
}
let map = '';
let body = '';
selected.forEach(function (i, k) {
const n = k + 1;
map += '[' + n + '] = ' + chunks[i].frame + '\n';
body += '[' + n + '] ' + chunks[i].text + '\n\n---\n\n';
});
const text =
'━━━ EVIDENCE MAP ━━━\n' + map +
'━━━━━━━━━━━━━━━━━━━━\n\n' + body.replace(/\n+---\n+$/, '\n');
const gaps = N - selected.length;
const stats = '`' + selected.length + '/' + N + ' passages · ' +
used.toLocaleString('en-US') + ' chars · ' + mode + ' · ' + views +
' query view' + (views === 1 ? '' : 's') +
(expansion.length ? ' · expanded' : '') +
(gaps > 0 ? ' · ' + gaps + ' passage(s) not sent' : ' · complete') + '`';
return { text: text, stats: stats, picked: selected.length, total:
N, ok: 'yes',
mode: mode, views: views, expanded: expansion.length };
param: selection
timeout: 60000
onFailure: ''
silent: true
- label: ADOPT THE FOLLOW UP EVIDENCE
type: calc
func: set
param: evidence
value: '{{selection.text}}'
format: ''
- label: HARDEN THE FOLLOW UP EVIDENCE
type: js
args: evidence
code: >-
// FULL PAGE VISION · RE-ARMOUR THE EVIDENCE
// The evidence arrives through a CALC step, and a CALC value is
interpolated, which the
// engine defines as consuming one layer of the `{{\` escape. So the
text that reaches this
// step has none left, and the prompt that receives it next would
execute any `{{page}}` or
// `{{serp x}}` the page happened to display. One layer goes back
on, and the escape is
// idempotent, so a value that still had one keeps exactly one.
function harden(value) {
return String(value == null ? '' : value).replace(/\{\{(?!\\)/g, '{{\\');
}
return harden(args.evidence);
param: evidence
timeout: 60000
onFailure: ''
silent: true
- label: REPORT THE FOLLOW UP SELECTION
type: say
message: 🔎 {{selection.stats}}
- label: APPLY CUSTOM FOLLOW UP
type: gpt
prompt: >-
Please ignore all previous instructions. Write only in
{{outputLanguage}}.
{{buildRule}}
Whatever stands under [REQUEST] is what you have to do. Apply it to the
[CURRENT RESULT] where that is what it means, and to the [EVIDENCE]
where it asks for something the current result does not hold.
[RULES]
- Do what the request says and output the result alone: no preamble, no
account of what you did, no closing remark. Where it asks for code or a
document, the answer is that code or that document and nothing else.
- Never reply that the record "does not contain" what was asked for. The
record contains the page; what was asked for is what you build out of
it. A request to build something can always be carried out, because the
material is already there.
- Take every word, number, heading, label, price, name and image
description from the record and reproduce it exactly.
- Use every piece of visual form the record gives you - typefaces,
sizes, colours, spacing, alignment, layout, order. Where the record is
silent, choose something consistent with what it does say and carry on;
do not stop to ask and do not leave a placeholder for a decision you can
make yourself.
- Where the record marks a region `[unreadable]` or `NOT READ`, put a
clearly marked placeholder at that spot and continue. A gap never stops
the work.
- If the request is a question rather than a task, answer it from the
record and name the frame it came from.
[REQUEST]:
{{change}}
[CURRENT RESULT]:
{{gpt}}
[EVIDENCE]:
{{citationRule}}{{evidence}}
[ANSWER]:
param: gpt
isolated: true
- label: AFTER CUSTOM FOLLOW UP
type: jump
to: VERIFY OUTPUT
- condition: '{{change}} = method'
label: SWITCH THE READING METHOD
type: group
steps:
- label: ANNOUNCE THE METHOD SWITCH
type: say
message: >-
🔄 The record and its index are still loaded — nothing is captured
again.
- label: GO BACK TO THE METHOD QUESTION
type: jump
to: CHOOSE HOW THE RECORD IS READ
- label: END
type: stop
- label: READING CANCELLED
type: say
message: >-
⛔ Stopped before reading. The frames were captured but not sent anywhere, so
no vision request was spent. Run the command again whenever you want to go
ahead.
- label: END AFTER CANCEL
type: stop
This automation command is created by a community member. HARPA AI team does not audit community commands.
Please review the command carefully and only install if you trust the creator.
All rights reserved © HARPA AI TECHNOLOGIES LLC, 2021 — 2026
Designed and engineered in Finland 🇫🇮