From 4a6b26c4b57f8aeb9aa8851d4c8183e110d5ccf5 Mon Sep 17 00:00:00 2001
From: Bridge Node 7 <291729790+Bridge-Node-7@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:12:46 -0700
Subject: [PATCH 01/28] Refine first-run decision experience
---
site/src/universal-ui.js | 617 +++++++++++++++++----------------------
1 file changed, 271 insertions(+), 346 deletions(-)
diff --git a/site/src/universal-ui.js b/site/src/universal-ui.js
index 24ba00f..5fa7635 100644
--- a/site/src/universal-ui.js
+++ b/site/src/universal-ui.js
@@ -4,11 +4,15 @@ import { DECISION_STORAGE_KEY, getBrowserStorage, saveDecision } from './lib/per
import { boundaryForInput } from './lib/input-boundaries.js';
import { deriveDecisionHinge } from './lib/decision-hinge.js';
-const MAX_SUGGESTIONS = Object.freeze({ choices: 3, goals: 4, futures: 4 });
-const SESSION_KEY = 'fde.universal.session.v1';
+const FORMAL_LIMITS = Object.freeze({
+ choices: DRAFT_TOPOLOGY_BOUNDS.strategies,
+ goals: DRAFT_TOPOLOGY_BOUNDS.objectives,
+ futures: DRAFT_TOPOLOGY_BOUNDS.scenarios,
+});
+const SESSION_KEY = 'fde.universal.session.v2';
const CONTEXT_KEY = 'fde.universal.context.v1';
const HANDOFF_KEY = 'fde.universal.handoff';
-const SESSION_VERSION = 1;
+const SESSION_VERSION = 2;
const KEYWORDS = {
goals: [
@@ -36,6 +40,8 @@ const KEYWORDS = {
const decisionPattern = /should i|should we|do i|do we|whether|which should|choose|decid(?:e|ing)\s+(?:between|whether)/i;
const informationPattern = /^(how much|what is|what's|when is|where is|who is|can you explain|explain|compare|what does|how does|tell me about)\b/i;
+const uncertainPattern = /\b(?:incomplete|unknown|uncertain|not sure|don't know|do not know|contested|conflicting|unverified|stale)\b/i;
+
function normalize(value) {
return String(value ?? '').replace(/\r\n?/g, '\n').trim();
}
@@ -59,7 +65,7 @@ function titleFrom(text) {
return sentence.length > 120 ? `${sentence.slice(0, 117)}…` : sentence;
}
-function unique(values, max) {
+function unique(values, max = Number.POSITIVE_INFINITY) {
return [...new Set(values.map((value) => normalize(value)).filter(Boolean))].slice(0, max);
}
@@ -88,6 +94,11 @@ function getIntent(text) {
return decisionPattern.test(clean) ? 'decision' : 'open';
}
+function extractDecisionCandidates(text) {
+ const sentences = normalize(text).split(/(?<=[.!?])\s+|\n+/).map((value) => value.trim()).filter(Boolean);
+ return unique(sentences.filter((value) => decisionPattern.test(value)).map((value) => value.replace(/[.!?]+$/, '').trim()));
+}
+
const criteriaClausePattern = /\b(?:care about|what matters|criteria|goals?|priorities|requirements?|matters?|important|must\s+(?:balance|protect|preserve|meet)|need(?:s)?\s+to\s+(?:balance|protect|preserve|meet))\b/i;
function extractListedChoices(text) {
@@ -100,28 +111,24 @@ function extractListedChoices(text) {
.split(/\s*,\s*(?:\b(?:or|and)\b\s*)?|\s+\b(?:or|and)\b\s+/i)
.map((value) => cleanChoice(value).replace(/\bwhich\s+should\s+(?:i|we)\s+choose\b.*$/i, '').trim())
.filter((value) => value.length >= 2);
- if (parts.length >= 3) return [...new Set(parts)];
+ if (parts.length >= 3) return unique(parts);
}
return [];
}
-function hasUnresolvedOptionList(text) {
- return extractListedChoices(text).length > MAX_SUGGESTIONS.choices;
-}
-
function extractChoices(text) {
const choices = [];
const clauses = normalize(text).split(/[.!?\n]+/).map((value) => value.trim()).filter(Boolean);
for (const clause of clauses) {
const decisionLike = decisionPattern.test(clause) || /^(?:should\b|choose|pick|select|decide)\b/i.test(clause);
if (!decisionLike) continue;
- for (const match of clause.matchAll(/(?:either\s+)?([^\n,.!?]{2,80})\s+(?:or|versus|vs\.?|instead of)\s+([^\n,.!?]{2,80})/gi)) {
+ for (const match of clause.matchAll(/(?:either\s+)?([^\n,.!?]{2,100})\s+(?:or|versus|vs\.?|instead of)\s+([^\n,.!?]{2,100})/gi)) {
choices.push(cleanChoice(match[1]), cleanChoice(match[2]));
}
}
- const numbered = [...text.matchAll(/(?:^|\n)\s*(?:\d+[.)]?|[-*•])\s+([^\n]{2,100})/g)]
+ const numbered = [...normalize(text).matchAll(/(?:^|\n)\s*(?:\d+[.)]?|[-*•])\s+([^\n]{2,120})/g)]
.map((match) => cleanChoice(match[1]));
- return unique([...choices, ...numbered], MAX_SUGGESTIONS.choices);
+ return unique([...choices, ...numbered]);
}
function phraseMatches(text, needle) {
@@ -133,7 +140,7 @@ function phraseMatches(text, needle) {
});
}
-function extractKeywordLabels(text, entries, max) {
+function extractKeywordLabels(text, entries, max = Number.POSITIVE_INFINITY) {
const matches = [];
for (const [needle, label] of entries) {
for (const span of phraseMatches(text, needle)) matches.push({ ...span, needle, label });
@@ -151,7 +158,7 @@ function extractKeywordLabels(text, entries, max) {
}
function extractGoals(text) {
- return extractKeywordLabels(text, KEYWORDS.goals, MAX_SUGGESTIONS.goals);
+ return extractKeywordLabels(text, KEYWORDS.goals);
}
function criterionLabel(value) {
@@ -173,7 +180,7 @@ function extractExplicitCriteria(text) {
for (const clause of clauses) {
if (!prefixCue.test(clause) && !suffixCue.test(clause)) continue;
- let body = clause
+ const body = clause
.replace(/^.*?\bcare about\b\s*/i, '')
.replace(/^.*?\bwhat matters(?: most)?(?: is| are)?\b\s*/i, '')
.replace(/^.*?\b(?:criteria|goals?|priorities|requirements?)\s+(?:are|include)\b\s*/i, '')
@@ -187,47 +194,45 @@ function extractExplicitCriteria(text) {
.filter((value) => value.length >= 2);
criteria.push(...items);
}
- return [...new Set(criteria)];
+ return unique(criteria);
}
function extractFutures(text) {
- return extractKeywordLabels(text, KEYWORDS.futures, MAX_SUGGESTIONS.futures);
+ return extractKeywordLabels(text, KEYWORDS.futures);
}
-function splitUserItems(text, max) {
+function splitUserItems(text) {
const clean = normalize(text);
if (!clean) return [];
- return unique(
- clean
- .split(/\n|;|,|\s+\bor\b\s+/i)
- .map((item) => item.replace(/^\s*(?:\d+[.)]?|[-*•])\s*/, '').trim()),
- max,
- );
+ return unique(clean
+ .split(/\n|;|,/)
+ .map((item) => item.replace(/^\s*(?:\d+[.)]?|[-*•])\s*/, '').trim())
+ .filter(Boolean));
}
export function draftFromInput(text) {
const clean = normalize(text);
const intent = getIntent(clean);
const listedChoices = extractListedChoices(clean);
- const detectedChoiceCount = listedChoices.length;
- const optionListAmbiguous = detectedChoiceCount > MAX_SUGGESTIONS.choices;
- const choices = optionListAmbiguous ? [] : unique([...extractChoices(clean), ...listedChoices], MAX_SUGGESTIONS.choices);
+ const choices = unique([...extractChoices(clean), ...listedChoices]);
const explicitCriteria = extractExplicitCriteria(clean);
- const detectedCriterionCount = explicitCriteria.length;
- const criteriaListAmbiguous = detectedCriterionCount > MAX_SUGGESTIONS.goals;
- const goals = criteriaListAmbiguous
- ? []
- : unique([...extractGoals(clean), ...explicitCriteria], MAX_SUGGESTIONS.goals);
+ const goals = unique([...extractGoals(clean), ...explicitCriteria]);
const futures = extractFutures(clean);
- const possibleDecision = intent !== 'multi' && (decisionPattern.test(clean) || (choices.length >= 2 && clean.includes('?'))) ? titleFrom(clean) : '';
+ const decisionCandidates = extractDecisionCandidates(clean);
+ const possibleDecision = intent !== 'multi' && decisionCandidates.length
+ ? decisionCandidates[0]
+ : intent !== 'multi' && choices.length >= 2 && clean.includes('?')
+ ? titleFrom(clean)
+ : '';
return {
startingPoint: clean,
intent,
possibleDecision,
- optionListAmbiguous,
- detectedChoiceCount,
- criteriaListAmbiguous,
- detectedCriterionCount,
+ decisionCandidates,
+ optionListAmbiguous: choices.length > FORMAL_LIMITS.choices.max,
+ detectedChoiceCount: choices.length,
+ criteriaListAmbiguous: goals.length > FORMAL_LIMITS.goals.max,
+ detectedCriterionCount: goals.length,
hingeCandidate: deriveDecisionHinge(clean),
choices,
goals,
@@ -238,31 +243,16 @@ export function draftFromInput(text) {
export function responseFor(state) {
const clean = normalize(state?.startingPoint);
const boundary = boundaryForInput(clean);
- if (boundary) {
- return { kind: 'boundary', title: boundary.title, body: boundary.body };
- }
- if (state?.optionListAmbiguous) {
- const count = Number(state.detectedChoiceCount) || 4;
- return { kind: 'question', question: `I found ${count} possible choices. Choose up to ${MAX_SUGGESTIONS.choices} to compare.` };
- }
- if (state?.criteriaListAmbiguous) {
- const count = Number(state.detectedCriterionCount) || (MAX_SUGGESTIONS.goals + 1);
- return { kind: 'question', question: `I found ${count} possible criteria. Choose up to ${MAX_SUGGESTIONS.goals} to keep.` };
- }
- if (!clean || (!state?.possibleDecision && state?.intent !== 'information' && state?.intent !== 'multi')) {
- return { kind: 'question', question: 'Which decision or question should we focus on?' };
- }
- if (state.intent === 'information') {
+ if (boundary) return { kind: 'boundary', title: boundary.title, body: boundary.body };
+ if (!clean) return { kind: 'empty' };
+ if (state?.intent === 'information') {
return {
kind: 'boundary',
title: 'FDE compares decisions; it does not retrieve outside facts.',
body: 'Gather the fact first, or state the decision that fact will inform.',
};
}
- if (state.intent === 'multi') {
- return { kind: 'question', question: 'Which decision or question should we focus on first?' };
- }
- return { kind: 'structure' };
+ return { kind: 'brief' };
}
function storage() {
@@ -293,151 +283,159 @@ function clearSession() {
try { storage()?.removeItem(SESSION_KEY); } catch { /* no-op */ }
}
-function hingeTarget(state) {
- const hinge = state.hingeCandidate;
- if (!hinge || state.hingeStatus) return '';
- if (['hard_requirement', 'deadline'].includes(hinge.basis_type)) {
- if (state.goals.includes(hinge.text)) return '';
- return state.goals.length < MAX_SUGGESTIONS.goals ? 'goals' : '';
+function supportableSection(label, semantic, value, fallback = '') {
+ if (Array.isArray(value)) {
+ const values = value.map(normalize).filter(Boolean);
+ if (!values.length && !fallback) return '';
+ const body = values.length
+ ? `
${values.map((item) => `${escapeHtml(item)} `).join('')} `
+ : `${escapeHtml(fallback)}
`;
+ return `${escapeHtml(label)} ${body} `;
}
- if (state.futures.includes(hinge.text)) return '';
- return state.futures.length < MAX_SUGGESTIONS.futures ? 'futures' : '';
+ const text = normalize(value);
+ if (!text && !fallback) return '';
+ return `${escapeHtml(label)} ${escapeHtml(text || fallback)}
`;
}
-function hingeQuestion(state) {
- if (['hard_requirement', 'deadline'].includes(state.hingeCandidate?.basis_type)) {
- return 'Does this condition need to be true for the decision?';
+function briefDecisionMarkup(state) {
+ if (state.decisionCandidates.length > 1) {
+ return supportableSection('Decisions identified', 'decision', state.decisionCandidates);
}
- return 'Could this condition change which choice holds up?';
-}
-
-function missingRequirement(state) {
- if (state.choices.length < DRAFT_TOPOLOGY_BOUNDS.strategies.min) return 'choices';
- if (state.goals.length < DRAFT_TOPOLOGY_BOUNDS.objectives.min) return 'goals';
- if (state.futures.length < DRAFT_TOPOLOGY_BOUNDS.scenarios.min) return 'futures';
- return '';
+ return supportableSection('Decision', 'decision', state.possibleDecision, 'The decision is not explicit yet.');
}
-function nextQuestion(state, kind) {
- if (kind === 'choices') return state.choices.length === 1 ? 'What is one other choice to compare?' : 'What choices should we compare?';
- if (kind === 'goals') return state.goals.length === 1 ? 'What else matters when comparing these choices?' : 'What matters most when comparing these choices?';
- if (kind === 'futures') return state.futures.length === 1 ? 'What else could change the choice?' : 'What conditions or uncertainties could change the choice?';
- return 'Which decision or question should we focus on?';
+function briefUnknowns(state) {
+ const unknowns = [];
+ if (!state.possibleDecision && state.decisionCandidates.length === 0) unknowns.push('The decision itself is not explicit yet.');
+ if (state.choices.length < FORMAL_LIMITS.choices.min) unknowns.push('The alternatives are not fully established.');
+ if (state.goals.length < FORMAL_LIMITS.goals.min) unknowns.push('What matters most is not fully established.');
+ if (uncertainPattern.test(state.startingPoint)) unknowns.push('Some supporting evidence remains uncertain.');
+ return unique(unknowns);
}
-function supportableSection(label, semantic, value) {
- if (Array.isArray(value)) {
- if (!value.length) return '';
- return `${escapeHtml(label)} ${value.map((item) => `${escapeHtml(item)} `).join('')} `;
+function nextUsefulMove(state) {
+ if (!state.possibleDecision && state.decisionCandidates.length === 0) {
+ return 'Refine only if you want FDE to isolate a formal decision.';
+ }
+ if (state.decisionCandidates.length > 1) {
+ return 'Refine or open Compare options when you want to focus one formal decision.';
+ }
+ if (state.choices.length < FORMAL_LIMITS.choices.min) {
+ return 'Refine only if you want to name alternatives for formal comparison.';
+ }
+ if (state.goals.length < FORMAL_LIMITS.goals.min) {
+ return 'Refine only if you want to make the comparison criteria explicit.';
}
- if (!normalize(value)) return '';
- return `${escapeHtml(label)} ${escapeHtml(value)}
`;
+ return 'Open Compare options when you want a formal deterministic comparison.';
}
function hingeMarkup(state) {
const hinge = state.hingeCandidate;
if (!hinge) return '';
- const status = state.hingeStatus === 'yes'
- ? 'You confirmed'
- : state.hingeStatus === 'no'
- ? 'You did not keep this as a controlling condition'
- : state.hingeStatus === 'not_sure'
- ? 'You marked this as uncertain'
- : 'FDE suggests';
return `
- ${escapeHtml(status)} From your words
+ Potential hinge From your words
This decision may turn on
${escapeHtml(hinge.text)}
`;
}
+function preservedMarkup(state) {
+ const messages = [];
+ if (state.decisionCandidates.length > 1) messages.push(`${state.decisionCandidates.length} decision questions preserved.`);
+ if (state.choices.length > FORMAL_LIMITS.choices.max) messages.push(`${state.choices.length} possible choices preserved.`);
+ if (state.goals.length > FORMAL_LIMITS.goals.max) messages.push(`${state.goals.length} criteria preserved.`);
+ if (!messages.length) return '';
+ return `Preserved context ${escapeHtml(messages.join(' '))} Formal comparison uses a human-selected bounded set without discarding the source context.
`;
+}
+
function entryMarkup(state, hasSavedDecision) {
return ``;
}
-function structureMarkup(state) {
- return `
+function briefMarkup(state, hasSavedDecision) {
+ const canCompare = state.choices.length >= FORMAL_LIMITS.choices.min || state.decisionCandidates.length > 1;
+ return `
Frontier Decision Engine
- What are you considering?
-
-
Decision structure Needs confirmation
+
+
Decision brief
+ ${briefDecisionMarkup(state)}
+ ${supportableSection('What matters', 'what_matters', state.goals, 'Not established yet.')}
+ ${supportableSection("What's still unclear", 'uncertainty', briefUnknowns(state), 'Nothing obvious from the supplied context.')}
+ ${supportableSection('Next useful move', 'next_useful_move', nextUsefulMove(state))}
${hingeMarkup(state)}
- ${supportableSection('Decision', 'decision', state.possibleDecision)}
- ${supportableSection('What matters', 'what_matters', state.goals)}
- ${supportableSection('Choices', 'options', state.choices)}
- ${supportableSection('What may change', 'what_may_change', state.futures)}
-
-
Is this the decision you want to evaluate?
-
Yes Adjust
+ ${preservedMarkup(state)}
+
+
Refine
+ ${canCompare ? '
Compare options ' : ''}
+ ${hasSavedDecision ? '
Continue saved work ' : ''}
+
Start another
-
Private by design. Your working decision stays in this browser unless you choose to export it.
+
See how FDE got here FDE organized only what appeared in the supplied context. The original text remains the source context and is not treated as evidence by itself.
${escapeHtml(state.startingPoint)}
+
Browser-local. Public or sanitized material only. The brief is working context, not a Decision Receipt.
`;
}
-function questionMarkup(state, question) {
- return `
- Frontier Decision Engine
- What are you considering?
-
- ${escapeHtml(question)}
- ${escapeHtml(question)}
-
- One useful answer is enough to continue.
-
- Continue Adjust original input
- Private by design. Your working decision stays in this browser unless you choose to export it.
-
-
- `;
+function checkboxList(name, values, selected, max) {
+ if (!values.length) return '
None detected yet. Add items below if you want to compare.
';
+ return `
${values.map((value, index) => {
+ const checked = selected.includes(value) ? ' checked' : '';
+ return ` ${escapeHtml(value)} `;
+ }).join('')}
`;
+}
+
+function decisionChoiceMarkup(state) {
+ const decisions = state.decisionCandidates.length ? state.decisionCandidates : state.possibleDecision ? [state.possibleDecision] : [];
+ if (decisions.length <= 1) {
+ return `
Decision `;
+ }
+ return `
Decision ${decisions.map((decision, index) => ` ${escapeHtml(decision)} `).join('')}
`;
}
-function hingeQuestionMarkup(state) {
+function compareMarkup(state) {
+ const selectedChoices = state.selectedChoices.length ? state.selectedChoices : state.choices.slice(0, FORMAL_LIMITS.choices.max);
+ const selectedGoals = state.selectedGoals.length ? state.selectedGoals : state.goals.slice(0, FORMAL_LIMITS.goals.max);
+ const selectedFutures = state.selectedFutures.length ? state.selectedFutures : state.futures.slice(0, FORMAL_LIMITS.futures.max);
const hinge = state.hingeCandidate;
- return `
+ return `
Frontier Decision Engine
- What are you considering?
-
- FDE suggests From your words
- ${escapeHtml(hingeQuestion(state))}
- ${escapeHtml(hinge?.text || '')}
-
-
Yes
-
No
-
Not sure
+
+
Compare options Optional. Set the bounded formal comparison here, then open Decision Lab.
+
+ ${decisionChoiceMarkup(state)}
+
Options Select 2–${FORMAL_LIMITS.choices.max} ${checkboxList('formal-choice', state.choices, selectedChoices, FORMAL_LIMITS.choices.max)}Add options, one per line
+
What matters Select 2–${FORMAL_LIMITS.goals.max} ${checkboxList('formal-goal', state.goals, selectedGoals, FORMAL_LIMITS.goals.max)}Add criteria, one per line
+
What may change Select 2–${FORMAL_LIMITS.futures.max} ${checkboxList('formal-future', state.futures, selectedFutures, FORMAL_LIMITS.futures.max)}Add conditions or uncertainties, one per line
+ ${hinge ? `
Potential hinge ${escapeHtml(hinge.text)}
Keep as important Leave uncertain Remove
` : ''}
-
Adjust original input
-
FDE is organizing your input. You remain the decision-maker.
-
-
+
+
Open Decision Lab Back to brief
+
Formal comparison remains deterministic and human-governed. The comparison informs; a person decides.
+
`;
}
-function boundaryMarkup(state, title, body, saved = false) {
- return `
+function boundaryMarkup(title, body, saved = false) {
+ return `
Frontier Decision Engine
- What are you considering?
-
- ${escapeHtml(title)}
+
+ ${escapeHtml(title)}
${escapeHtml(body)}
-
- Private by design. Your working decision stays in this browser unless you choose to export it.
+
+ Browser-local. Public or sanitized material only.
`;
@@ -449,23 +447,22 @@ export function renderUniversalDecisionExperience(root) {
startingPoint: normalize(restored.startingPoint || ''),
intent: restored.intent || '',
possibleDecision: restored.possibleDecision || '',
+ decisionCandidates: Array.isArray(restored.decisionCandidates) ? restored.decisionCandidates : [],
optionListAmbiguous: Boolean(restored.optionListAmbiguous),
detectedChoiceCount: Number(restored.detectedChoiceCount) || 0,
criteriaListAmbiguous: Boolean(restored.criteriaListAmbiguous),
detectedCriterionCount: Number(restored.detectedCriterionCount) || 0,
hingeCandidate: restored.hingeCandidate || null,
- hingeStatus: restored.hingeStatus || '',
- hingeApplied: Boolean(restored.hingeApplied),
- choices: Array.isArray(restored.choices) ? restored.choices.slice(0, MAX_SUGGESTIONS.choices) : [],
- goals: Array.isArray(restored.goals) ? restored.goals.slice(0, MAX_SUGGESTIONS.goals) : [],
- futures: Array.isArray(restored.futures) ? restored.futures.slice(0, MAX_SUGGESTIONS.futures) : [],
- view: ['entry', 'structure', 'question', 'boundary'].includes(restored.view) ? restored.view : 'entry',
- questionKind: restored.questionKind || '',
- question: restored.question || '',
+ choices: Array.isArray(restored.choices) ? restored.choices : [],
+ goals: Array.isArray(restored.goals) ? restored.goals : [],
+ futures: Array.isArray(restored.futures) ? restored.futures : [],
+ selectedChoices: Array.isArray(restored.selectedChoices) ? restored.selectedChoices : [],
+ selectedGoals: Array.isArray(restored.selectedGoals) ? restored.selectedGoals : [],
+ selectedFutures: Array.isArray(restored.selectedFutures) ? restored.selectedFutures : [],
+ view: ['entry', 'brief', 'compare', 'boundary'].includes(restored.view) ? restored.view : 'entry',
boundaryTitle: restored.boundaryTitle || '',
boundaryBody: restored.boundaryBody || '',
savedBoundary: Boolean(restored.savedBoundary),
- answerDraft: normalize(restored.answerDraft || ''),
};
const hasSavedDecision = () => Boolean(getBrowserStorage(globalThis)?.getItem?.(DECISION_STORAGE_KEY));
@@ -475,241 +472,159 @@ export function renderUniversalDecisionExperience(root) {
state.boundaryTitle = title;
state.boundaryBody = body;
state.savedBoundary = saved;
- state.questionKind = '';
- state.question = '';
- state.answerDraft = '';
- saveSession(state);
- paint('#universal-response-title');
- }
-
- function setQuestion(kind, question = '') {
- state.view = 'question';
- state.questionKind = kind;
- state.question = question || nextQuestion(state, kind);
- state.answerDraft = '';
saveSession(state);
paint('#universal-response-title');
}
function returnToEntry() {
state.view = 'entry';
- state.questionKind = '';
- state.question = '';
state.boundaryTitle = '';
state.boundaryBody = '';
state.savedBoundary = false;
- state.answerDraft = '';
- state.hingeCandidate = null;
- state.hingeStatus = '';
- state.hingeApplied = false;
saveSession(state);
paint('#universal-input');
}
- function handoffWhenReady() {
- const hingeNext = hingeTarget(state);
- if (hingeNext) {
- setQuestion('hinge', hingeQuestion(state));
- return;
- }
- const missing = missingRequirement(state);
- if (missing) {
- setQuestion(missing);
- return;
- }
- if (hasSavedDecision()) {
- setBoundary('A saved FDE decision already exists.', 'Open Decision Lab to review it before replacing anything.', true);
- return;
- }
- const counts = { objectives: state.goals.length, strategies: state.choices.length, scenarios: state.futures.length };
- const withinBounds = counts.objectives >= DRAFT_TOPOLOGY_BOUNDS.objectives.min
- && counts.objectives <= DRAFT_TOPOLOGY_BOUNDS.objectives.max
- && counts.strategies >= DRAFT_TOPOLOGY_BOUNDS.strategies.min
- && counts.strategies <= DRAFT_TOPOLOGY_BOUNDS.strategies.max
- && counts.scenarios >= DRAFT_TOPOLOGY_BOUNDS.scenarios.min
- && counts.scenarios <= DRAFT_TOPOLOGY_BOUNDS.scenarios.max;
- if (!withinBounds || !state.possibleDecision) {
- setQuestion(missing || 'decision');
- return;
- }
- const decision = createGuidedDecisionCase({
- objectiveCount: counts.objectives,
- strategyCount: counts.strategies,
- scenarioCount: counts.scenarios,
- });
- decision.question = state.possibleDecision;
- decision.title = state.possibleDecision.slice(0, 120);
- state.goals.forEach((label, index) => { decision.objectives[index].label = label; });
- state.choices.forEach((label, index) => { decision.strategies[index].label = label; });
- state.futures.forEach((label, index) => { decision.scenarios[index].label = label; });
- const result = saveDecision(getBrowserStorage(globalThis), decision, null);
- if (!result.ok) {
- setBoundary('FDE could not save this browser-local draft.', 'Keep this page open, review browser storage settings, then try again.');
- return;
- }
+ function startAnother() {
clearSession();
- try {
- storage()?.setItem(CONTEXT_KEY, JSON.stringify({ version: 1, startingPoint: state.startingPoint }));
- storage()?.setItem(HANDOFF_KEY, '1');
- } catch { /* optional */ }
- location.hash = '#/decision';
+ Object.assign(state, {
+ startingPoint: '', intent: '', possibleDecision: '', decisionCandidates: [],
+ optionListAmbiguous: false, detectedChoiceCount: 0, criteriaListAmbiguous: false,
+ detectedCriterionCount: 0, hingeCandidate: null, choices: [], goals: [], futures: [],
+ selectedChoices: [], selectedGoals: [], selectedFutures: [], view: 'entry',
+ boundaryTitle: '', boundaryBody: '', savedBoundary: false,
+ });
+ paint('#universal-input');
}
- function showInputLimit() {
- const message = root.querySelector('#universal-limit');
- if (message) {
- message.hidden = false;
- message.textContent = `This is longer than FDE can safely structure at once. Choose or paste the section containing the decision, question, or conditions you want to evaluate, and keep it under ${GUIDED_MAX_INPUT_CHARS.toLocaleString()} characters.`;
+ function showInputLimit(message = '') {
+ const element = root.querySelector('#universal-limit') || root.querySelector('#universal-compare-validation');
+ if (element) {
+ element.hidden = false;
+ element.textContent = message || `This is longer than FDE can safely structure at once. Keep the working context under ${GUIDED_MAX_INPUT_CHARS.toLocaleString()} characters.`;
}
- root.querySelector('#universal-input')?.focus({ preventScroll: true });
}
function analyzeEntry() {
const raw = String(root.querySelector('#universal-input')?.value || '');
if (raw.length > GUIDED_MAX_INPUT_CHARS) {
- state.startingPoint = normalize(raw.slice(0, GUIDED_MAX_INPUT_CHARS + 1));
- saveSession(state);
- showInputLimit();
+ showInputLimit(`This is longer than FDE can safely structure at once. Keep the working context under ${GUIDED_MAX_INPUT_CHARS.toLocaleString()} characters.`);
+ root.querySelector('#universal-input')?.focus({ preventScroll: true });
return;
}
state.startingPoint = normalize(raw);
const draft = draftFromInput(state.startingPoint);
- Object.assign(state, draft, { hingeStatus: '', hingeApplied: false });
+ Object.assign(state, draft, { selectedChoices: [], selectedGoals: [], selectedFutures: [] });
const response = responseFor(state);
- if (response.kind === 'question') {
- setQuestion(state.optionListAmbiguous ? 'choices' : state.criteriaListAmbiguous ? 'goals' : 'decision', response.question);
+ if (response.kind === 'empty') {
+ showInputLimit('Enter a decision, question, or context to continue.');
+ root.querySelector('#universal-input')?.focus({ preventScroll: true });
return;
}
if (response.kind === 'boundary') {
setBoundary(response.title, response.body);
return;
}
- state.view = 'structure';
- state.questionKind = '';
- state.question = '';
- state.answerDraft = '';
+ state.view = 'brief';
saveSession(state);
paint('#universal-response-title');
}
- function applyHingeAnswer(answer) {
- const hinge = state.hingeCandidate;
- if (!hinge || !['yes', 'no', 'not_sure'].includes(answer)) return;
- state.hingeStatus = answer;
- state.hingeApplied = false;
- if (answer === 'yes' && ['hard_requirement', 'deadline'].includes(hinge.basis_type) && state.goals.length < MAX_SUGGESTIONS.goals) {
- const before = state.goals.length;
- state.goals = unique([...state.goals, hinge.text], MAX_SUGGESTIONS.goals);
- state.hingeApplied = state.goals.length > before;
- } else if (answer === 'yes' && state.futures.length < MAX_SUGGESTIONS.futures) {
- const before = state.futures.length;
- state.futures = unique([...state.futures, hinge.text], MAX_SUGGESTIONS.futures);
- state.hingeApplied = state.futures.length > before;
- } else if (answer === 'not_sure' && state.futures.length < MAX_SUGGESTIONS.futures) {
- const before = state.futures.length;
- state.futures = unique([...state.futures, hinge.text], MAX_SUGGESTIONS.futures);
- state.hingeApplied = state.futures.length > before;
- }
- state.questionKind = '';
- state.question = '';
- state.answerDraft = '';
- saveSession(state);
- handoffWhenReady();
+ function checkedValues(name) {
+ return [...root.querySelectorAll(`input[name="${name}"]:checked`)].map((element) => normalize(element.value)).filter(Boolean);
}
- function applyQuestionAnswer() {
- const raw = String(root.querySelector('#universal-input')?.value || '');
- if (raw.length > GUIDED_MAX_INPUT_CHARS) {
- showInputLimit();
- return;
- }
- const answer = normalize(raw);
- state.answerDraft = answer;
- if (!answer) {
- saveSession(state);
- paint('#universal-response-title');
+ function enforceCheckboxLimit(name, max) {
+ root.querySelectorAll(`input[name="${name}"]`).forEach((checkbox) => checkbox.addEventListener('change', () => {
+ const checked = checkedValues(name);
+ if (checked.length > max) {
+ checkbox.checked = false;
+ const label = name === 'formal-choice' ? 'options' : name === 'formal-goal' ? 'criteria' : 'conditions';
+ showInputLimit(`Select no more than ${max} ${label} for one formal comparison.`);
+ } else {
+ const message = root.querySelector('#universal-compare-validation');
+ if (message) message.hidden = true;
+ }
+ }));
+ }
+
+ function formalDecision() {
+ return normalize(root.querySelector('input[name="formal-decision"]:checked')?.value || root.querySelector('#universal-decision-text')?.value || state.possibleDecision);
+ }
+
+ function formalSelections(name, additionsId, max) {
+ return unique([...checkedValues(name), ...splitUserItems(root.querySelector(additionsId)?.value || '')], max);
+ }
+
+ function openDecisionLab() {
+ if (hasSavedDecision()) {
+ setBoundary('A saved FDE decision already exists.', 'Open Decision Lab to review it before replacing anything.', true);
return;
}
+ const decisionText = formalDecision();
+ const selectedChoices = formalSelections('formal-choice', '#universal-add-choices', FORMAL_LIMITS.choices.max);
+ const selectedGoals = formalSelections('formal-goal', '#universal-add-goals', FORMAL_LIMITS.goals.max);
+ const selectedFutures = formalSelections('formal-future', '#universal-add-futures', FORMAL_LIMITS.futures.max);
+ const hingeStatus = root.querySelector('input[name="hinge-status"]:checked')?.value || '';
+ const hinge = state.hingeCandidate;
- // A complete new decision entered during a follow-up replaces the prior
- // intake context instead of inheriting choices, goals, or uncertainties.
- const replacementDraft = draftFromInput(answer);
- const answerDefinesNewFocus = Boolean(replacementDraft.possibleDecision)
- || replacementDraft.intent === 'multi'
- || replacementDraft.intent === 'information';
- if (answerDefinesNewFocus) {
- Object.assign(state, replacementDraft, { hingeStatus: '', hingeApplied: false });
- const replacementResponse = responseFor(state);
- if (replacementResponse.kind === 'question') {
- setQuestion(state.optionListAmbiguous ? 'choices' : state.criteriaListAmbiguous ? 'goals' : 'decision', replacementResponse.question);
- return;
+ if (hinge && ['yes', 'not_sure'].includes(hingeStatus)) {
+ if (hingeStatus === 'yes' && ['hard_requirement', 'deadline'].includes(hinge.basis_type)) {
+ if (!selectedGoals.includes(hinge.text) && selectedGoals.length < FORMAL_LIMITS.goals.max) selectedGoals.push(hinge.text);
+ } else if (!selectedFutures.includes(hinge.text) && selectedFutures.length < FORMAL_LIMITS.futures.max) {
+ selectedFutures.push(hinge.text);
}
- if (replacementResponse.kind === 'boundary') {
- setBoundary(replacementResponse.title, replacementResponse.body);
- return;
- }
- state.view = 'structure';
- state.questionKind = '';
- state.question = '';
- state.answerDraft = '';
- saveSession(state);
- paint('#universal-response-title');
- return;
}
- if (state.questionKind === 'decision') {
- const draft = draftFromInput(answer);
- state.startingPoint = state.startingPoint ? `${state.startingPoint}\n${answer}` : answer;
- Object.assign(state, {
- intent: draft.intent,
- possibleDecision: draft.possibleDecision,
- choices: unique([...state.choices, ...draft.choices], MAX_SUGGESTIONS.choices),
- goals: unique([...state.goals, ...draft.goals], MAX_SUGGESTIONS.goals),
- futures: unique([...state.futures, ...draft.futures], MAX_SUGGESTIONS.futures),
- });
- if (!state.possibleDecision) {
- state.question = 'Which decision should FDE evaluate?';
- state.answerDraft = '';
- saveSession(state);
- paint('#universal-response-title');
- return;
- }
- state.view = 'structure';
- state.questionKind = '';
- state.question = '';
- state.answerDraft = '';
- saveSession(state);
- paint('#universal-response-title');
+ state.selectedChoices = selectedChoices;
+ state.selectedGoals = selectedGoals;
+ state.selectedFutures = selectedFutures;
+ saveSession(state);
+
+ const errors = [];
+ if (!decisionText) errors.push('State one decision to compare.');
+ if (selectedChoices.length < FORMAL_LIMITS.choices.min) errors.push(`Select at least ${FORMAL_LIMITS.choices.min} options.`);
+ if (selectedGoals.length < FORMAL_LIMITS.goals.min) errors.push(`Select at least ${FORMAL_LIMITS.goals.min} criteria.`);
+ if (selectedFutures.length < FORMAL_LIMITS.futures.min) errors.push(`Select at least ${FORMAL_LIMITS.futures.min} conditions or uncertainties.`);
+ if (errors.length) {
+ showInputLimit(errors.join(' '));
+ root.querySelector('#universal-compare-validation')?.focus?.({ preventScroll: true });
return;
}
- if (state.questionKind === 'choices') state.choices = unique([...state.choices, ...splitUserItems(answer, MAX_SUGGESTIONS.choices)], MAX_SUGGESTIONS.choices);
- if (state.questionKind === 'goals') {
- state.goals = unique([...state.goals, ...splitUserItems(answer, MAX_SUGGESTIONS.goals)], MAX_SUGGESTIONS.goals);
- state.criteriaListAmbiguous = false;
- state.detectedCriterionCount = state.goals.length;
- }
- if (state.questionKind === 'futures') state.futures = unique([...state.futures, ...splitUserItems(answer, MAX_SUGGESTIONS.futures)], MAX_SUGGESTIONS.futures);
- state.answerDraft = '';
- const missing = missingRequirement(state);
- if (missing) {
- setQuestion(missing);
+
+ const decision = createGuidedDecisionCase({
+ objectiveCount: selectedGoals.length,
+ strategyCount: selectedChoices.length,
+ scenarioCount: selectedFutures.length,
+ });
+ decision.question = decisionText;
+ decision.title = decisionText.slice(0, 120);
+ selectedGoals.forEach((label, index) => { decision.objectives[index].label = label; });
+ selectedChoices.forEach((label, index) => { decision.strategies[index].label = label; });
+ selectedFutures.forEach((label, index) => { decision.scenarios[index].label = label; });
+
+ const result = saveDecision(getBrowserStorage(globalThis), decision, null);
+ if (!result.ok) {
+ setBoundary('FDE could not save this browser-local draft.', 'Keep this page open, review browser storage settings, then try again.');
return;
}
- handoffWhenReady();
+ clearSession();
+ try {
+ storage()?.setItem(CONTEXT_KEY, JSON.stringify({ version: 1, startingPoint: state.startingPoint }));
+ storage()?.setItem(HANDOFF_KEY, '1');
+ } catch { /* optional */ }
+ location.hash = '#/decision';
}
function paint(focusSelector = '') {
- if (state.view === 'structure') root.innerHTML = structureMarkup(state);
- else if (state.view === 'question' && state.questionKind === 'hinge') root.innerHTML = hingeQuestionMarkup(state);
- else if (state.view === 'question') root.innerHTML = questionMarkup(state, state.question || nextQuestion(state, state.questionKind));
- else if (state.view === 'boundary') root.innerHTML = boundaryMarkup(state, state.boundaryTitle, state.boundaryBody, state.savedBoundary);
+ if (state.view === 'brief') root.innerHTML = briefMarkup(state, hasSavedDecision());
+ else if (state.view === 'compare') root.innerHTML = compareMarkup(state);
+ else if (state.view === 'boundary') root.innerHTML = boundaryMarkup(state.boundaryTitle, state.boundaryBody, state.savedBoundary);
else root.innerHTML = entryMarkup(state, hasSavedDecision());
root.querySelector('#universal-input')?.addEventListener('input', (event) => {
const rawValue = String(event.currentTarget.value || '');
- const value = rawValue.slice(0, GUIDED_MAX_INPUT_CHARS + 1);
- if (state.view === 'entry') state.startingPoint = value;
- else state.answerDraft = value;
+ state.startingPoint = rawValue.slice(0, GUIDED_MAX_INPUT_CHARS + 1);
const limit = root.querySelector('#universal-limit');
if (limit && rawValue.length <= GUIDED_MAX_INPUT_CHARS) limit.hidden = true;
saveSession(state);
@@ -717,20 +632,30 @@ export function renderUniversalDecisionExperience(root) {
root.querySelector('#universal-input')?.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
- if (state.view === 'entry') analyzeEntry();
- else if (state.view === 'question') applyQuestionAnswer();
+ analyzeEntry();
}
});
- root.querySelector('#universal-analyze')?.addEventListener('click', () => {
- if (state.view === 'entry') analyzeEntry();
- else if (state.view === 'question') applyQuestionAnswer();
- });
+ root.querySelector('#universal-analyze')?.addEventListener('click', analyzeEntry);
root.querySelector('#universal-adjust')?.addEventListener('click', returnToEntry);
- root.querySelector('#universal-confirm')?.addEventListener('click', handoffWhenReady);
- root.querySelectorAll('[data-hinge-answer]').forEach((button) => button.addEventListener('click', () => applyHingeAnswer(button.dataset.hingeAnswer)));
+ root.querySelector('#universal-refine')?.addEventListener('click', returnToEntry);
+ root.querySelector('#universal-start-over')?.addEventListener('click', startAnother);
+ root.querySelector('#universal-compare')?.addEventListener('click', () => {
+ state.view = 'compare';
+ saveSession(state);
+ paint('#universal-response-title');
+ });
+ root.querySelector('#universal-back-brief')?.addEventListener('click', () => {
+ state.view = 'brief';
+ saveSession(state);
+ paint('#universal-response-title');
+ });
+ root.querySelector('#universal-open-lab')?.addEventListener('click', openDecisionLab);
+ enforceCheckboxLimit('formal-choice', FORMAL_LIMITS.choices.max);
+ enforceCheckboxLimit('formal-goal', FORMAL_LIMITS.goals.max);
+ enforceCheckboxLimit('formal-future', FORMAL_LIMITS.futures.max);
if (focusSelector) root.querySelector(focusSelector)?.focus({ preventScroll: true });
}
paint(state.view === 'entry' ? '#universal-title' : '#universal-response-title');
-}
+}
\ No newline at end of file
From 65bd6127fc30c4f27924038105521a867b6e354a Mon Sep 17 00:00:00 2001
From: Bridge Node 7 <291729790+Bridge-Node-7@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:12:49 -0700
Subject: [PATCH 02/28] Refine first-run decision styling
---
site/assets/universal-decision.css | 75 +++++++++++++++++-------------
1 file changed, 42 insertions(+), 33 deletions(-)
diff --git a/site/assets/universal-decision.css b/site/assets/universal-decision.css
index 09dfc7b..d7161b5 100644
--- a/site/assets/universal-decision.css
+++ b/site/assets/universal-decision.css
@@ -1,46 +1,55 @@
:root{--surface-glow:rgba(117,214,255,.08);--surface-border:rgba(117,214,255,.18)}
.universal-hero{width:100%;max-width:920px;margin:clamp(2.75rem,7vw,6.5rem) auto 5rem;padding:0 .5rem}
.universal-hero .eyebrow{display:block;text-align:center}
-.universal-hero h1{margin:.65rem auto .8rem;font-size:clamp(2.7rem,5vw,5rem);line-height:1.02;letter-spacing:-.05em;max-width:900px;text-align:center;text-wrap:balance}
-.universal-subtitle{max-width:720px;margin:0 auto 2rem;color:var(--muted);font-size:clamp(1rem,1.4vw,1.18rem);line-height:1.6;text-align:center}
+.universal-hero h1{margin:.65rem auto 1.25rem;font-size:clamp(2.7rem,5vw,4.7rem);line-height:1.01;letter-spacing:-.052em;max-width:none;text-align:center;text-wrap:balance}
+.universal-front-door h1{white-space:nowrap}
.universal-entry{max-width:780px;margin:0 auto}
-#universal-input{width:100%;min-height:220px;resize:vertical;border:1px solid var(--line-strong);border-radius:20px;background:var(--surface);font:inherit;font-size:1rem;line-height:1.6;padding:1rem 1.05rem;box-shadow:0 18px 55px rgba(0,0,0,.12)}
+#universal-input{width:100%;min-height:240px;resize:vertical;border:1px solid var(--line-strong);border-radius:20px;background:var(--surface);font:inherit;font-size:1rem;line-height:1.6;padding:1rem 1.05rem;box-shadow:0 18px 55px rgba(0,0,0,.12)}
#universal-input:focus-visible{outline:3px solid var(--focus,Highlight);outline-offset:3px}
-.universal-entry .help,.universal-question .help{margin:.6rem 0 1rem;color:var(--muted)}
-.universal-actions{display:flex;gap:.65rem;flex-wrap:wrap;align-items:center}
+.universal-actions{display:flex;gap:.65rem;flex-wrap:wrap;align-items:center;margin-top:1rem}
.universal-actions .primary{min-height:48px;min-width:132px}
-.universal-lab-link,.universal-return,.universal-trust{margin:.9rem 0 0;line-height:1.5}
-.universal-lab-link a,.universal-return a{font-weight:800;color:var(--text)}
-.universal-trust{color:var(--muted);font-size:.88rem}
+.universal-actions .quiet,.universal-actions .button.quiet{background:transparent;color:var(--muted)}
+.universal-return,.universal-trust{margin:.85rem 0 0;line-height:1.5}
+.universal-return a{font-weight:800;color:var(--text)}
+.universal-trust{color:var(--muted);font-size:.82rem}
.universal-post-input{max-width:920px;margin-top:clamp(2.25rem,5vw,4.5rem)}
-.universal-post-input>h1{font-size:clamp(2.2rem,4vw,3.6rem);margin-bottom:1.8rem}
-.universal-structure,.universal-question,.universal-boundary{border:1px solid var(--line-strong);background:var(--surface);border-radius:22px;padding:clamp(1rem,2.5vw,1.6rem);box-shadow:0 20px 65px rgba(0,0,0,.12)}
+.universal-structure,.universal-boundary{border:1px solid var(--line-strong);background:var(--surface);border-radius:24px;padding:clamp(1.05rem,3vw,1.8rem);box-shadow:0 20px 65px rgba(0,0,0,.12)}
.universal-structure-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;padding-bottom:1rem;border-bottom:1px solid var(--line)}
-.universal-structure-head h2,.universal-question h2,.universal-boundary h2{margin:0;font-size:clamp(1.5rem,2.8vw,2rem);line-height:1.15}
-.universal-badge{display:inline-flex;align-items:center;min-height:30px;padding:.32rem .55rem;border:1px solid var(--line-strong);border-radius:999px;color:var(--muted);font-size:.72rem;font-weight:850;letter-spacing:.05em;text-transform:uppercase;white-space:nowrap}
+.universal-structure-head h1,.universal-boundary h1{margin:0;text-align:left;font-size:clamp(1.7rem,3vw,2.35rem);line-height:1.08;letter-spacing:-.035em;white-space:normal}
+.universal-structure-head p{margin:.55rem 0 0;color:var(--muted);max-width:68ch}
.universal-field{padding:1.05rem 0;border-bottom:1px solid var(--line)}
-.universal-field h3,.universal-confirmation h3{margin:0 0 .5rem;font-size:.88rem;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
+.universal-field h3{margin:0 0 .5rem;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.universal-field p{margin:0;font-size:1.08rem;line-height:1.55}
.universal-field ul{margin:.2rem 0 0;padding-left:1.2rem;display:grid;gap:.35rem;line-height:1.5}
-.universal-confirmation{padding-top:1.2rem}
-.universal-confirmation h3{font-size:1.05rem;letter-spacing:0;text-transform:none;color:var(--text)}
-.universal-confirm-row{display:flex;gap:.55rem;flex-wrap:wrap}
-.universal-confirm-row button{min-height:44px}
-.universal-question textarea{margin-top:1.2rem}
-.universal-boundary p{color:var(--muted);line-height:1.6}
-@media(max-width:620px){.universal-hero{margin-top:2rem;margin-bottom:3rem;padding:0}.universal-hero h1{font-size:clamp(2.35rem,11vw,3.5rem);line-height:1.04}.universal-subtitle{font-size:1rem;margin-bottom:1.4rem}.universal-actions,.universal-confirm-row{align-items:stretch;flex-direction:column}.universal-actions button,.universal-actions .button,.universal-confirm-row button{width:100%}.universal-structure,.universal-question,.universal-boundary{border-radius:18px}.universal-structure-head{align-items:flex-start;flex-direction:column}.universal-badge{align-self:flex-start}#universal-input{min-height:190px}}
-@media(max-width:360px){.universal-hero h1{font-size:2.2rem}.universal-structure,.universal-question,.universal-boundary{padding:.9rem}}
-@media(forced-colors:active){#universal-input,.universal-structure,.universal-question,.universal-boundary,.universal-badge{box-shadow:none;border:1px solid CanvasText}#universal-input:focus-visible{outline:3px solid Highlight}}
-@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
-
-.universal-advanced{margin:.9rem 0 0;color:var(--muted)}
-.universal-advanced summary{cursor:pointer;font-weight:800;color:var(--text);width:max-content}
-.universal-advanced p{margin:.55rem 0 0}
-.universal-advanced a{font-weight:800;color:var(--text)}
-.universal-limit{margin:.65rem 0 1rem;padding:.75rem .85rem;border:1px solid var(--line-strong);border-radius:12px;color:var(--text);background:var(--surface)}
+.universal-empty{color:var(--muted)}
+.universal-brief-actions{margin-top:1.25rem}
+.universal-limit{margin:.65rem 0 0;padding:.75rem .85rem;border:1px solid var(--line-strong);border-radius:12px;color:var(--text);background:var(--surface-2)}
.universal-hinge{margin:.2rem 0 .1rem;padding:1rem 0 1.1rem;border-bottom:1px solid var(--line)}
-.universal-hinge h3{margin:.45rem 0 .5rem;font-size:.88rem;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
-.universal-hinge p,.universal-hinge-quote{margin:0;font-size:1.08rem;line-height:1.55}
-.universal-hinge-meta{display:flex;gap:.45rem;flex-wrap:wrap;color:var(--muted);font-size:.76rem;font-weight:800}
+.universal-hinge h3{margin:.45rem 0 .5rem;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
+.universal-hinge p,.universal-hinge-quote{margin:0;font-size:1rem;line-height:1.55}
+.universal-hinge-meta{display:flex;gap:.45rem;flex-wrap:wrap;color:var(--muted);font-size:.74rem;font-weight:800}
.universal-hinge-meta span{display:inline-flex;padding:.22rem .45rem;border:1px solid var(--line);border-radius:999px}
-.universal-hinge-question .universal-hinge-quote{margin:1rem 0 1.15rem;padding:1rem;border-left:3px solid var(--line-strong)}
+.universal-preserved{margin:1rem 0 0;padding:.9rem 1rem;border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.025)}
+.universal-preserved strong{display:block;margin-bottom:.25rem}
+.universal-preserved p{margin:0;color:var(--muted);font-size:.9rem;line-height:1.5}
+.universal-trace{margin-top:1rem;border-top:1px solid var(--line);padding-top:1rem}
+.universal-trace summary{cursor:pointer;font-weight:800;color:var(--muted);width:max-content;max-width:100%}
+.universal-trace p{margin:.75rem 0;color:var(--muted);font-size:.9rem}
+.universal-trace pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:.7rem 0 0;padding:.9rem;border:1px solid var(--line);border-radius:14px;background:var(--surface-2);color:var(--muted);font:inherit;font-size:.88rem;line-height:1.55;max-height:18rem;overflow:auto}
+.universal-boundary p{color:var(--muted);line-height:1.6;margin:1rem 0 0}
+.universal-compare-grid{display:grid;gap:.85rem;margin-top:1rem}
+.universal-compare-field{min-width:0;border:1px solid var(--line);border-radius:18px;padding:1rem;background:rgba(255,255,255,.025)}
+label.universal-compare-field{display:grid;gap:.55rem;font-weight:800}
+.universal-compare-field legend{padding:0 .2rem;font-weight:850;color:var(--text)}
+.universal-compare-field legend small{margin-left:.35rem;color:var(--muted);font-weight:600}
+.universal-check-list,.universal-radio-list{display:grid;gap:.55rem;margin:.35rem 0 .75rem}
+.universal-check-list label,.universal-radio-list label{display:flex;align-items:flex-start;gap:.55rem;min-height:36px;color:var(--text);font-weight:650}
+.universal-check-list input,.universal-radio-list input{width:auto;min-width:18px;margin-top:.18rem;accent-color:var(--accent)}
+.universal-add{display:grid;gap:.4rem;color:var(--muted);font-size:.86rem;font-weight:750;margin-top:.75rem}
+.universal-add textarea{min-height:82px;resize:vertical}
+.universal-compare .universal-hinge-quote{margin:.45rem 0 .7rem;padding:.75rem .85rem;border-left:3px solid var(--line-strong);background:var(--surface-2)}
+@media(min-width:780px){.universal-compare-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.universal-compare-field:first-child{grid-column:1/-1}}
+@media(max-width:620px){.universal-hero{margin-top:2rem;margin-bottom:3rem;padding:0}.universal-front-door h1{font-size:clamp(2.35rem,11vw,3.5rem);line-height:1.04;white-space:normal}.universal-actions{align-items:stretch;flex-direction:column}.universal-actions button,.universal-actions .button{width:100%}.universal-structure,.universal-boundary{border-radius:18px}.universal-structure-head{align-items:flex-start;flex-direction:column}#universal-input{min-height:210px}.universal-check-list label,.universal-radio-list label{min-height:44px;align-items:center}.universal-check-list input,.universal-radio-list input{margin-top:0}}
+@media(max-width:360px){.universal-front-door h1{font-size:2.2rem}.universal-structure,.universal-boundary{padding:.9rem}}
+@media(forced-colors:active){#universal-input,.universal-structure,.universal-boundary,.universal-compare-field,.universal-preserved{box-shadow:none;border:1px solid CanvasText}.universal-check-list input,.universal-radio-list input{forced-color-adjust:auto}#universal-input:focus-visible{outline:3px solid Highlight}}
+@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
\ No newline at end of file
From 83d649a78a3e8a35789084c601c6f8444353c096 Mon Sep 17 00:00:00 2001
From: Bridge Node 7 <291729790+Bridge-Node-7@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:12:52 -0700
Subject: [PATCH 03/28] Update first-run decision tests
---
tests/universal-response.test.js | 236 ++++++++++---------------------
1 file changed, 71 insertions(+), 165 deletions(-)
diff --git a/tests/universal-response.test.js b/tests/universal-response.test.js
index b86985e..56d4623 100644
--- a/tests/universal-response.test.js
+++ b/tests/universal-response.test.js
@@ -2,27 +2,24 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { draftFromInput, responseFor } from '../site/src/universal-ui.js';
-test('clear decision input produces only supportable deterministic structure', () => {
+test('clear decision input produces a supportable brief candidate', () => {
const draft = draftFromInput('Should we qualify an alternate source or redesign around the dependency? Schedule risk and resilience matter, but qualification may be late.');
assert.equal(draft.intent, 'decision');
assert.equal(draft.possibleDecision, 'Should we qualify an alternate source or redesign around the dependency');
assert.deepEqual(draft.choices, ['qualify an alternate source', 'redesign around the dependency']);
assert.deepEqual(draft.goals, ['Schedule risk', 'Resilience', 'Qualification']);
assert.deepEqual(draft.futures, ['Timing gets worse']);
- assert.equal(responseFor(draft).kind, 'structure');
+ assert.equal(responseFor(draft).kind, 'brief');
});
-test('criteria lists do not override explicit binary choices from ordinary language', () => {
- const input = 'Should we qualify a second gallium nitride wafer supplier in Japan or keep our current Chinese supplier? We care about cost, schedule risk, and DFARS compliance.';
- const draft = draftFromInput(input);
- assert.equal(draft.intent, 'decision');
- assert.equal(draft.optionListAmbiguous, false);
+test('criteria lists do not override explicit binary choices', () => {
+ const draft = draftFromInput('Should we qualify a second gallium nitride wafer supplier in Japan or keep our current Chinese supplier? We care about cost, schedule risk, and DFARS compliance.');
assert.deepEqual(draft.choices, [
'qualify a second gallium nitride wafer supplier in Japan',
'keep our current Chinese supplier',
]);
assert.deepEqual(draft.goals, ['Cost', 'Schedule risk', 'Compliance']);
- assert.equal(responseFor(draft).kind, 'structure');
+ assert.equal(responseFor(draft).kind, 'brief');
});
test('decision title prefers the actual decision over leading blocker context', () => {
@@ -35,15 +32,18 @@ test('institutional should-question produces readable choice labels', () => {
assert.deepEqual(draft.choices, ['Ministry — expand port capacity', 'defer investment']);
});
-test('sparse input produces exactly one useful clarification question', () => {
- for (const input of ['', 'qualification evidence incomplete', 'The mission dependency is unclear.']) {
- const response = responseFor(draftFromInput(input));
- assert.equal(response.kind, 'question');
- assert.match(response.question, /which decision or question/i);
- assert.equal(Object.keys(response).filter((key) => key === 'question').length, 1);
+test('sparse in-scope context returns a partial brief instead of a compulsory question', () => {
+ for (const input of ['qualification evidence incomplete', 'The mission dependency is unclear.']) {
+ const draft = draftFromInput(input);
+ assert.equal(responseFor(draft).kind, 'brief', input);
+ assert.equal(draft.possibleDecision, '', input);
}
});
+test('empty input remains an inline entry-state validation case', () => {
+ assert.equal(responseFor(draftFromInput('')).kind, 'empty');
+});
+
test('information request returns an honest capability boundary and useful next action', () => {
const response = responseFor(draftFromInput('What is the current spot price of gallium?'));
assert.equal(response.kind, 'boundary');
@@ -51,13 +51,16 @@ test('information request returns an honest capability boundary and useful next
assert.match(response.body, /gather the fact|state the decision/i);
});
-test('multiple decisions ask for one focus instead of fabricating a combined model', () => {
- const draft = draftFromInput('Should we qualify a second source? Should we redesign around the dependency?');
+test('multiple decisions are preserved instead of forcing an immediate focus question', () => {
+ const draft = draftFromInput('Should we qualify a second source? Should we redesign around the dependency? Should we build strategic inventory?');
assert.equal(draft.intent, 'multi');
assert.equal(draft.possibleDecision, '');
- const response = responseFor(draft);
- assert.equal(response.kind, 'question');
- assert.match(response.question, /focus on first/i);
+ assert.deepEqual(draft.decisionCandidates, [
+ 'Should we qualify a second source',
+ 'Should we redesign around the dependency',
+ 'Should we build strategic inventory',
+ ]);
+ assert.equal(responseFor(draft).kind, 'brief');
});
test('human input remains inert context and extracted fields remain explicit', () => {
@@ -70,55 +73,68 @@ test('human input remains inert context and extracted fields remain explicit', (
assert.ok(draft.goals.includes('Safety'));
});
-test('listed choices are preserved when they fit the comparison bound', () => {
+test('listed choices are preserved when they fit the formal comparison bound', () => {
for (const input of [
'Should we qualify Supplier Alpha, Supplier Bravo, or Supplier Charlie?',
'We can dual-source, stockpile, or redesign. Which should we choose?',
'Should we qualify the incumbent material, a substitute material, or a redesigned subsystem?',
]) {
const draft = draftFromInput(input);
- assert.equal(draft.optionListAmbiguous, false, input);
assert.equal(draft.detectedChoiceCount, 3, input);
assert.equal(draft.choices.length, 3, input);
- assert.equal(responseFor(draft).kind, 'structure', input);
+ assert.equal(responseFor(draft).kind, 'brief', input);
}
});
-test('oversized listed choices ask the user to choose up to the supported bound', () => {
+test('oversized choice sets are preserved in capture state rather than truncated', () => {
const input = 'Choose between Supplier Alpha, Supplier Bravo, an alternate material, a reserve, or subsystem redesign.';
const draft = draftFromInput(input);
assert.equal(draft.optionListAmbiguous, true);
assert.equal(draft.detectedChoiceCount, 5);
- assert.deepEqual(draft.choices, []);
- const response = responseFor(draft);
- assert.equal(response.kind, 'question');
- assert.equal(response.question, 'I found 5 possible choices. Choose up to 3 to compare.');
-});
-
-test('binary option extraction remains deterministic', () => {
- const draft = draftFromInput('Should we qualify an alternate source or redesign around the dependency?');
- assert.equal(draft.optionListAmbiguous, false);
- assert.deepEqual(draft.choices, ['qualify an alternate source', 'redesign around the dependency']);
- assert.equal(responseFor(draft).kind, 'structure');
+ assert.deepEqual(draft.choices, ['Supplier Alpha', 'Supplier Bravo', 'an alternate material', 'a reserve', 'subsystem redesign']);
+ assert.equal(responseFor(draft).kind, 'brief');
});
-test('generated oversized-choice corpus never exposes a strict partial option set', () => {
+test('generated oversized-choice corpus never drops source candidates', () => {
let cases = 0;
for (let count = 4; count <= 7; count += 1) {
- for (let variant = 0; variant < 60; variant += 1) {
+ for (let variant = 0; variant < 40; variant += 1) {
const options = Array.from({ length: count }, (_, index) => `qualification-path-${variant}-${index + 1}`);
- const prefix = variant % 2 === 0 ? 'Should we choose ' : 'Choose between ';
- const separator = variant % 3 === 0 ? ', ' : variant % 3 === 1 ? ', ' : ', ';
- const input = `${prefix}${options.slice(0, -1).join(separator)}, or ${options.at(-1)}?`;
+ const input = `Choose between ${options.slice(0, -1).join(', ')}, or ${options.at(-1)}?`;
const draft = draftFromInput(input);
assert.equal(draft.optionListAmbiguous, true, input);
assert.equal(draft.detectedChoiceCount, count, input);
- assert.equal(draft.choices.length, 0, input);
- assert.equal(responseFor(draft).question, `I found ${count} possible choices. Choose up to 3 to compare.`, input);
+ assert.equal(draft.choices.length, count, input);
+ assert.equal(responseFor(draft).kind, 'brief', input);
cases += 1;
}
}
- assert.ok(cases >= 200);
+ assert.ok(cases >= 160);
+});
+
+test('explicit criteria outside the keyword dictionary remain visible', () => {
+ const draft = draftFromInput('Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, and corrosion resistance matter.');
+ assert.deepEqual(draft.goals, ['Data residency', 'Capex', 'Measurement traceability', 'Corrosion resistance']);
+});
+
+test('oversized criteria sets remain preserved in capture state', () => {
+ const draft = draftFromInput('Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, corrosion resistance, repairability, maintainability, and schedule matter.');
+ assert.equal(draft.criteriaListAmbiguous, true);
+ assert.equal(draft.detectedCriterionCount, 7);
+ assert.equal(draft.goals.length, 7);
+ assert.equal(responseFor(draft).kind, 'brief');
+});
+
+test('informational versus language is not converted into a decision choice set', () => {
+ const information = draftFromInput('Explain qualification versus redesign for a new engineer.');
+ assert.equal(information.intent, 'information');
+ assert.deepEqual(information.choices, []);
+ assert.equal(responseFor(information).kind, 'boundary');
+
+ const decision = draftFromInput('Should we qualify Supplier Alpha versus Supplier Bravo?');
+ assert.equal(decision.intent, 'decision');
+ assert.deepEqual(decision.choices, ['qualify Supplier Alpha', 'Supplier Bravo']);
+ assert.equal(responseFor(decision).kind, 'brief');
});
test('out-of-scope personal treatment decisions fail closed', () => {
@@ -134,66 +150,54 @@ test('out-of-scope personal treatment decisions fail closed', () => {
assert.equal(response.kind, 'boundary', input);
assert.equal(response.title, 'This decision is outside FDE’s comparison scope.', input);
assert.match(response.body, /technical, organizational, mission, and strategic/i, input);
- assert.match(response.body, /qualified support/i, input);
}
});
test('out-of-scope severe personal restriction decisions fail closed', () => {
- const examples = [
+ for (const input of [
'Should I stop eating to lose weight faster?',
'Should I fast for five days or seven days?',
'Should I skip all meals this week?',
'Should I starve myself to reach my goal sooner?',
- ];
- for (const input of examples) {
+ ]) {
const response = responseFor(draftFromInput(input));
assert.equal(response.kind, 'boundary', input);
assert.equal(response.title, 'This decision is outside FDE’s comparison scope.', input);
- assert.match(response.body, /technical, organizational, mission, and strategic/i, input);
}
});
test('out-of-scope personal dosing comparisons fail closed', () => {
- const examples = [
+ for (const input of [
'Should I take 2 ibuprofen or 4 ibuprofen for this?',
'Should I take 500 mg or 1000 mg acetaminophen?',
'Should I use one dose or two doses of cough medicine?',
'Should I take one tablet or three tablets of this medication?',
- ];
- for (const input of examples) {
+ ]) {
const response = responseFor(draftFromInput(input));
assert.equal(response.kind, 'boundary', input);
assert.equal(response.title, 'This decision is outside FDE’s comparison scope.', input);
- assert.match(response.body, /technical, organizational, mission, and strategic/i, input);
- assert.match(response.body, /qualified support/i, input);
}
});
test('out-of-scope immediate personal safety decisions fail closed', () => {
- const examples = [
+ for (const input of [
'Should I drive myself to the ER or wait it out?',
'Should I go to urgent care now or wait until morning?',
'Should I call 911 or wait it out?',
'Should I wait it out or head to the emergency room?',
- ];
- for (const input of examples) {
+ ]) {
const response = responseFor(draftFromInput(input));
assert.equal(response.kind, 'boundary', input);
assert.equal(response.title, 'This decision is outside FDE’s comparison scope.', input);
- assert.match(response.body, /technical, organizational, mission, and strategic/i, input);
- assert.match(response.body, /qualified real-time support/i, input);
}
});
-test('ordinary non-medical decisions are not caught by personal safety boundaries', () => {
+test('ordinary organizational decisions are not caught by personal boundaries', () => {
for (const input of [
'Should we qualify an alternate source or redesign around the dependency to save money?',
'Should we reduce qualification scope or delay the readiness gate?',
'Should we hold deployment pending evidence or proceed at the next readiness window?',
- 'Should we qualify the faster source or the more reliable source?',
'Should we field 2 redundant ground nodes or 4 redundant ground nodes?',
- 'Should the mission processor reserve 2 GB or 4 GB of memory?',
- 'Should we delay the mission-control software release or proceed at the current readiness gate?',
]) {
const response = responseFor(draftFromInput(input));
assert.notEqual(response.kind === 'boundary' && response.title === 'This decision is outside FDE’s comparison scope.', true, input);
@@ -210,113 +214,15 @@ test('self-directed crisis language fails closed before ordinary decision struct
'I want to die.',
'I want to end it all.',
'end it all',
- 'Should I cut myself or call someone?',
- 'I cannot keep living like this.',
]) {
const response = responseFor(draftFromInput(input));
assert.equal(response.kind, 'boundary', input);
- assert.equal(response.title, 'This decision is outside FDE’s comparison scope.', input);
- assert.match(response.body, /does not compare or optimize self-harm/i, input);
- assert.match(response.body, /988/i, input);
- }
-});
-
-test('specific crisis language and neutral personal scope both fail closed without blocking supplier idiom', () => {
- const crisis = responseFor(draftFromInput('Should I just end it all?'));
- assert.equal(crisis.kind, 'boundary');
- assert.match(crisis.body, /988/);
-
- for (const input of [
- 'Should I stop living in London and move to Leeds?',
- 'Should I give up on life coaching as a career or continue?',
- ]) {
- const personal = responseFor(draftFromInput(input));
- assert.equal(personal.kind, 'boundary', input);
- assert.match(personal.body, /personal-life decisions/i, input);
- assert.match(personal.body, /not a clinical judgment/i, input);
- assert.doesNotMatch(personal.body, /does not compare or optimize self-harm/i, input);
- }
-
- const supplier = responseFor(draftFromInput('Should we end it all with this supplier or renegotiate?'));
- assert.notEqual(supplier.kind === 'boundary' && supplier.title === 'This decision is outside FDE’s comparison scope.', true);
-});
-
-test('organizational prevention decisions are not mistaken for first-person self-harm', () => {
- for (const input of [
- 'Should the ministry fund suicide prevention hotlines or school counselors?',
- 'Should we expand suicide prevention training or crisis-response staffing?',
- 'Should the program measure self-harm prevention outcomes or referral completion?',
- ]) {
- const response = responseFor(draftFromInput(input));
- assert.notEqual(response.kind === 'boundary' && response.title === 'This decision is outside FDE’s comparison scope.', true, input);
- }
-});
-
-test('ordinary engineering and business harm language does not trigger the personal boundary', () => {
- for (const input of [
- 'Should we terminate the failed qualification run or restart it?',
- 'Could this dependency harm mission readiness enough to change the decision?',
- 'Should we terminate the failed verification run or continue collecting evidence?',
- ]) {
- const response = responseFor(draftFromInput(input));
- assert.notEqual(response.kind === 'boundary' && response.title === 'This decision is outside FDE’s comparison scope.', true, input);
- }
-});
-
-test('mission criteria use bounded phrase matching without substring false positives', () => {
- const costa = draftFromInput('Should we qualify Costara Materials or retain the incumbent source? Reliability and resilience matter.');
- assert.deepEqual(costa.choices, ['qualify Costara Materials', 'retain the incumbent source']);
- assert.equal(costa.goals.includes('Cost'), false);
- assert.deepEqual(costa.goals, ['Reliability', 'Resilience']);
-
- const mission = draftFromInput('Should we impose export controls or negotiate supply agreements with allies? National security, cost, and time matter.');
- assert.equal(mission.optionListAmbiguous, false);
- assert.deepEqual(mission.choices, ['impose export controls', 'negotiate supply agreements with allies']);
- assert.deepEqual(mission.goals, ['National security', 'Cost', 'Time']);
- assert.equal(responseFor(mission).kind, 'structure');
-});
-
-test('ordinary criteria-list forms do not override explicit choices', () => {
- for (const input of [
- 'Should we qualify or redesign? Cost, schedule risk, and readiness matter.',
- 'Should we qualify or redesign? Cost, schedule risk, and readiness are important.',
- 'Should we qualify or redesign? Our priorities are cost, readiness, and resilience.',
- 'Should we qualify or redesign? The decision must balance cost, readiness, and interoperability.',
- ]) {
- const draft = draftFromInput(input);
- assert.equal(draft.optionListAmbiguous, false, input);
- assert.deepEqual(draft.choices, ['qualify', 'redesign'], input);
- assert.equal(responseFor(draft).kind, 'structure', input);
+ assert.match(response.body, /988|emergency|support/i, input);
}
});
-
-
-test('explicit criteria outside the keyword dictionary remain visible for human confirmation', () => {
- const draft = draftFromInput('Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, and corrosion resistance matter.');
- assert.equal(draft.criteriaListAmbiguous, false);
- assert.deepEqual(draft.goals, ['Data residency', 'Capex', 'Measurement traceability', 'Corrosion resistance']);
- assert.equal(responseFor(draft).kind, 'structure');
-});
-
-test('oversized explicit criteria sets ask for bounded human selection instead of silently truncating', () => {
- const draft = draftFromInput('Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, corrosion resistance, and repairability matter.');
- assert.equal(draft.criteriaListAmbiguous, true);
- assert.equal(draft.detectedCriterionCount, 5);
- assert.deepEqual(draft.goals, []);
- const response = responseFor(draft);
- assert.equal(response.kind, 'question');
- assert.equal(response.question, 'I found 5 possible criteria. Choose up to 4 to keep.');
-});
-
-test('informational versus language is not converted into a decision choice set', () => {
- const information = draftFromInput('Explain qualification versus redesign for a new engineer.');
- assert.equal(information.intent, 'information');
- assert.deepEqual(information.choices, []);
- assert.equal(responseFor(information).kind, 'boundary');
-
- const decision = draftFromInput('Should we qualify Supplier Alpha versus Supplier Bravo?');
- assert.equal(decision.intent, 'decision');
- assert.deepEqual(decision.choices, ['qualify Supplier Alpha', 'Supplier Bravo']);
- assert.equal(responseFor(decision).kind, 'structure');
-});
+test('supplier language using end-it-all idiom is not misclassified as personal crisis', () => {
+ const input = 'Should we end it all with this supplier or renegotiate the contract? Cost and resilience matter.';
+ const response = responseFor(draftFromInput(input));
+ assert.equal(response.kind, 'brief');
+});
\ No newline at end of file
From b4813881a0591e1185e754110e494ed5438b08e8 Mon Sep 17 00:00:00 2001
From: Bridge Node 7 <291729790+Bridge-Node-7@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:12:54 -0700
Subject: [PATCH 04/28] Verify decision-brief first-run flow
---
scripts/browser_guided_framing_e2e.py | 475 ++++++++++++--------------
1 file changed, 218 insertions(+), 257 deletions(-)
diff --git a/scripts/browser_guided_framing_e2e.py b/scripts/browser_guided_framing_e2e.py
index 5488fcb..42945ee 100644
--- a/scripts/browser_guided_framing_e2e.py
+++ b/scripts/browser_guided_framing_e2e.py
@@ -1,274 +1,235 @@
#!/usr/bin/env python3
-"""Focused browser verification for the frictionless FDE first-run surface."""
+"""First-run browser verification for the Decision-Brief-first FDE experience."""
from __future__ import annotations
-import functools
-import http.server
import os
-import socketserver
-import threading
-from pathlib import Path
+from urllib.parse import urlparse
-from playwright.sync_api import sync_playwright
+try:
+ from playwright.sync_api import sync_playwright
+except ImportError as exc: # pragma: no cover
+ raise SystemExit(
+ "Browser E2E requires Playwright. Run: node scripts/run-python.mjs -m pip install "
+ "-r requirements-dev.txt and node scripts/run-python.mjs -m playwright install chromium"
+ ) from exc
-ROOT = Path(__file__).resolve().parents[1]
-SITE = ROOT / "site"
-SESSION_KEY = "fde.universal.session.v1"
-DECISION_KEY = "fde.decision.autosave.v0.2.11"
-GUIDED_SESSION_KEY = "fde.guided-framing.session.v1"
+from browser_e2e import browser_executable, install_static_route, native_http_available, start_static_server
+SESSION_KEY = "fde.universal.session.v2"
+OLD_SESSION_KEY = "fde.universal.session.v1"
+DECISION_STORAGE_KEY = "fde.decision.v1"
-class QuietHandler(http.server.SimpleHTTPRequestHandler):
- def log_message(self, *_args):
- pass
+def launch_browser(playwright):
+ executable = browser_executable()
+ kwargs = {"headless": True}
+ if executable:
+ kwargs["executable_path"] = executable
+ return playwright.chromium.launch(**kwargs)
-def browser_executable() -> str | None:
- return os.environ.get("CHROME_BIN") or None
+
+def reset(page, base: str) -> None:
+ page.goto(base, wait_until="networkidle")
+ page.evaluate("localStorage.clear(); sessionStorage.clear()")
+ page.goto(base, wait_until="networkidle")
+
+
+def assert_first_view(page) -> None:
+ assert page.locator("#universal-title").inner_text() == "What are you considering?"
+ field = page.locator("#universal-input")
+ assert field.get_attribute("placeholder") == "Type or paste anything relevant…"
+ assert page.get_by_role("button", name="Continue").count() == 1
+ assert page.get_by_text("Advanced paths", exact=True).count() == 0
+ assert page.get_by_text("Share the situation, decision, question, or context in your own words.", exact=True).count() == 0
+
+
+def assert_brief(page) -> None:
+ assert page.locator("#universal-response-title").inner_text() == "Decision brief"
+ assert page.locator('[data-fde-field="decision"]').count() == 1
+ assert page.locator('[data-fde-field="what_matters"]').count() == 1
+ assert page.locator('[data-fde-field="uncertainty"]').count() == 1
+ assert page.locator('[data-fde-field="next_useful_move"]').count() == 1
+ assert page.locator(".universal-question").count() == 0
def run() -> None:
- handler = functools.partial(QuietHandler, directory=str(SITE))
- with socketserver.TCPServer(("127.0.0.1", 0), handler) as server:
- thread = threading.Thread(target=server.serve_forever, daemon=True)
- thread.start()
- base = f"http://127.0.0.1:{server.server_address[1]}/"
- try:
- with sync_playwright() as p:
- launch = {"headless": True}
- executable = browser_executable()
- if executable:
- launch["executable_path"] = executable
- browser = p.chromium.launch(**launch)
- try:
- context = browser.new_context(viewport={"width": 390, "height": 844})
- remote_requests: list[str] = []
- context.on("request", lambda request: remote_requests.append(request.url) if not request.url.startswith(base) else None)
- page = context.new_page()
- page.goto(base, wait_until="networkidle")
-
- assert page.title() == "Frontier Decision Engine"
- assert page.locator("h1").inner_text() == "What are you considering?"
- assert page.get_by_text("Share the situation, decision, question, or context in your own words.", exact=True).is_visible()
- assert page.get_by_role("button", name="Continue").is_visible()
- assert page.get_by_text("Advanced paths", exact=True).is_visible()
- page.get_by_text("Advanced paths", exact=True).click()
- assert page.get_by_role("link", name="Already know the decision and choices? Open Decision Lab →").is_visible()
- assert page.get_by_role("link", name="Need more help framing the decision? Use guided framing →").is_visible()
- assert page.get_by_text("Private by design. Your working decision stays in this browser unless you choose to export it.", exact=True).is_visible()
- assert page.locator("#universal-input").get_attribute("placeholder") == "Decision, choices, criteria, uncertainties, notes, or context…"
- assert page.locator(".universal-surface").count() == 0
- assert "Decision Map" not in page.locator("main").inner_text()
- assert "Bring the whole mess" not in page.locator("body").inner_text()
- assert page.locator("#theme-toggle").inner_text() == "Appearance"
- assert "Current:" in (page.locator("#theme-toggle").get_attribute("aria-label") or "")
- assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
-
- # Long input stays visible and produces an explicit bounded-processing message.
- long_input = "x" * 12001
- page.locator("#universal-input").fill(long_input)
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-limit").is_visible()
- assert len(page.locator("#universal-input").input_value()) == len(long_input)
-
- # A grounded controlling condition is shown before schema completion.
- page.locator("#universal-input").fill("Should we qualify Supplier A or Supplier B? November is our integration window.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("[data-fde-field='decision_hinge']").is_visible()
- assert page.get_by_text("November is our integration window", exact=True).is_visible()
- page.get_by_role("button", name="Yes").click()
- assert page.locator("#universal-response-title").inner_text() == "Does this condition need to be true for the decision?"
- page.get_by_role("button", name="Yes").click()
- assert "What else matters" in page.locator("#universal-response-title").inner_text()
- page.get_by_role("button", name="Adjust original input").click()
-
- # Explicit prohibitive hard requirements remain eligible hinge candidates.
- page.locator("#universal-input").fill("Should we proceed with this design? The system must not exceed 10 watts.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("[data-fde-field='decision_hinge']").is_visible()
- assert page.get_by_text("must not exceed 10 watts", exact=True).is_visible()
- page.locator("#universal-adjust").click()
-
- # Sparse input yields one question, not an invalid state or empty structural cards.
- page.locator("#universal-input").fill("qualification evidence incomplete")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Which decision or question should we focus on?"
- assert page.locator("[data-fde-field='next_required_input']").count() == 1
- assert page.locator("[data-fde-field='decision']").count() == 0
- assert "Invalid input" not in page.locator("body").inner_text()
-
- page.get_by_role("button", name="Adjust original input").click()
- clear_input = "Should we qualify an alternate source or redesign around the dependency? Schedule risk and resilience matter, but qualification may be late. "
- page.locator("#universal-input").fill(clear_input)
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("Needs confirmation", exact=True).is_visible()
- assert page.locator("[data-fde-field='decision']").is_visible()
- assert page.locator("[data-fde-field='what_matters']").is_visible()
- assert page.locator("[data-fde-field='options']").is_visible()
- assert page.locator("[data-fde-field='what_may_change']").is_visible()
- assert page.get_by_text("qualify an alternate source", exact=True).is_visible()
- assert page.get_by_text("redesign around the dependency", exact=True).is_visible()
- assert page.get_by_text("Schedule risk", exact=True).is_visible()
- assert page.get_by_text("Resilience", exact=True).is_visible()
- assert page.get_by_text("Timing gets worse", exact=True).is_visible()
- assert page.locator("script").filter(has_text="alert(1)").count() == 0
-
- # Confirmation requests only the next required input.
- page.get_by_role("button", name="Yes").click()
- assert page.locator("#universal-response-title").inner_text() == "What else could change the choice?"
- page.locator("#universal-input").fill("Requirements change")
-
- # Saved-work protection: never silently replace a Decision Lab draft.
- page.evaluate(f"localStorage.setItem('{DECISION_KEY}', JSON.stringify({{sentinel:'keep-me'}}))")
- page.get_by_role("button", name="Continue").click()
- assert page.evaluate(f"localStorage.getItem('{DECISION_KEY}')") == '{"sentinel":"keep-me"}'
- assert page.locator("#universal-response-title").inner_text() == "A saved FDE decision already exists."
- assert page.get_by_role("link", name="Open Decision Lab →").is_visible()
-
- # A completely new decision entered during a follow-up must not inherit
- # criteria or options from the abandoned decision.
- page.evaluate(f"localStorage.removeItem('{DECISION_KEY}')")
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- diligence_input = "Should we qualify a second gallium nitride wafer supplier in Japan or keep our current Chinese supplier? We care about cost, schedule risk, and DFARS compliance."
- page.locator("#universal-input").fill(diligence_input)
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("Cost", exact=True).is_visible()
- assert page.get_by_text("Schedule risk", exact=True).is_visible()
- assert page.get_by_text("Compliance", exact=True).is_visible()
- page.get_by_role("button", name="Yes").click()
- assert page.locator("#universal-response-title").inner_text() == "What conditions or uncertainties could change the choice?"
- page.locator("#universal-input").fill("Should we retire the legacy test stand?")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("Should we retire the legacy test stand", exact=True).is_visible()
- current_text = page.locator("main").inner_text()
- assert "Cost" not in current_text
- assert "Schedule risk" not in current_text
- assert "Compliance" not in current_text
- assert "Japanese supplier" not in current_text
- assert "Chinese supplier" not in current_text
-
- # Information-request UAT is a separate first-run scenario. Clear only the
- # tab-scoped intake session; the persistence behavior itself is tested below.
- page.evaluate(f"localStorage.removeItem('{DECISION_KEY}')")
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- page.locator("#universal-input").fill("What is the current spot price of gallium?")
- page.get_by_role("button", name="Continue").click()
- assert "does not retrieve outside facts" in page.locator("#universal-response-title").inner_text().lower()
- assert "Gather the fact first" in page.locator("main").inner_text()
-
- # Self-directed crisis language fails closed before ordinary decision structuring.
- page.evaluate(f"localStorage.removeItem('{DECISION_KEY}')")
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- page.locator("#universal-input").fill("I want to die.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "This decision is outside FDE’s comparison scope."
- assert "does not compare or optimize self-harm" in page.locator("main").inner_text()
- assert "988" in page.locator("main").inner_text()
-
- # Organizational prevention decisions remain valid decision inputs.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should the ministry fund suicide prevention hotlines or school counselors?")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
-
- # Oversized choice sets get an explicit bounded-selection prompt.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Choose between Supplier Alpha, Supplier Bravo, an alternate material, a reserve, or subsystem redesign.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "I found 5 possible choices. Choose up to 3 to compare."
-
- # Mission-focused criteria remain choices/criteria, not an ambiguous option list.
- page.evaluate(f"sessionStorage.removeItem('{SESSION_KEY}')")
- page.goto(base, wait_until="networkidle")
- page.locator("#universal-input").fill("Should we impose export controls or negotiate supply agreements with allies? National security, cost, and time matter.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- assert page.get_by_text("National security", exact=True).is_visible()
- assert page.get_by_text("Cost", exact=True).is_visible()
- assert page.get_by_text("Time", exact=True).is_visible()
- assert page.get_by_text("impose export controls", exact=True).is_visible()
- assert page.get_by_text("negotiate supply agreements with allies", exact=True).is_visible()
-
- # Explicit criteria outside the built-in vocabulary stay visible for human confirmation.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, and corrosion resistance matter.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
- for criterion in ("Data residency", "Capex", "Measurement traceability", "Corrosion resistance"):
- assert page.get_by_text(criterion, exact=True).is_visible()
-
- # More criteria than the bounded draft can hold must never be silently truncated.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should we qualify Supplier Alpha or Supplier Bravo? Data residency, capex, measurement traceability, corrosion resistance, and repairability matter.")
- page.get_by_role("button", name="Continue").click()
- assert page.locator("#universal-response-title").inner_text() == "I found 5 possible criteria. Choose up to 4 to keep."
-
- # Informational comparison language remains informational rather than inventing a choice set.
- page.get_by_role("button", name="Adjust original input").click()
- page.locator("#universal-input").fill("Explain qualification versus redesign for a new engineer.")
- page.get_by_role("button", name="Continue").click()
- assert "does not retrieve outside facts" in page.locator("#universal-response-title").inner_text().lower()
-
- # Ctrl/Cmd + Enter activates Continue.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Should we qualify the alternate source or hold for evidence? Mission safety and cost exposure matter. Schedule slips and demand changes are possible.")
- page.locator("#universal-input").press("Control+Enter")
- assert page.locator("#universal-response-title").inner_text() == "Decision structure"
-
- # Refresh preserves in-progress first-run work.
- page.get_by_role("button", name="Adjust").click()
- page.locator("#universal-input").fill("Refresh should not erase this qualification context.")
- assert page.evaluate(f"Boolean(sessionStorage.getItem('{SESSION_KEY}'))")
- page.reload(wait_until="networkidle")
- assert page.locator("#universal-input").input_value() == "Refresh should not erase this qualification context."
-
- # Guided framing enforces the same input boundary.
- page.evaluate(f"sessionStorage.removeItem('{GUIDED_SESSION_KEY}')")
- page.goto(f"{base}#/framing", wait_until="networkidle")
- page.locator("#guided-question").wait_for(state="visible")
- page.locator("#guided-intake").fill("Should I cut myself or call someone?")
- page.locator("#guided-start").click()
- assert "This decision is outside FDE’s comparison scope." in page.locator("#guided-status").inner_text()
- assert "988" in page.locator("#guided-status").inner_text()
- assert page.locator("#guided-question").inner_text() == "What decision context are you facing?"
-
- # Decision Lab enforces the same boundary before stage advancement.
- page.goto(f"{base}#/decision/new", wait_until="networkidle")
- page.locator("#decision-question").fill("I am suicidal and deciding what to do.")
- page.locator('[data-decision-stage="0"] [data-stage-next]').click()
- validation = page.locator('[data-stage-validation="0"]')
- assert "This decision is outside FDE’s comparison scope." in validation.inner_text()
- assert "988" in validation.inner_text()
- assert page.locator("#decision-question").get_attribute("aria-invalid") == "true"
-
- assert not remote_requests, f"FDE made unexpected remote requests: {remote_requests}"
- context.close()
-
- # Appearance follows system, then remains operable in dark mode.
- theme = browser.new_context(viewport={"width": 1280, "height": 900}, color_scheme="light")
- theme_page = theme.new_page()
- theme_page.goto(base, wait_until="networkidle")
- assert theme_page.locator("html").get_attribute("data-theme") == "light"
- assert theme_page.locator("html").get_attribute("data-theme-preference") == "system"
- assert theme_page.locator("#theme-toggle").inner_text() == "Appearance"
- theme_page.emulate_media(color_scheme="dark")
- theme_page.locator('html[data-theme="dark"]').wait_for(state="attached")
- assert theme_page.locator("html").get_attribute("data-theme-preference") == "system"
- assert theme_page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
- theme.close()
- finally:
- browser.close()
- finally:
- server.shutdown()
- thread.join(timeout=5)
+ server, native_base = start_static_server()
+ try:
+ with sync_playwright() as playwright:
+ browser = launch_browser(playwright)
+ try:
+ base = native_base
+ if not native_http_available(browser, base, attempts=2):
+ base = "http://fde.test/"
+
+ context = browser.new_context(viewport={"width": 1280, "height": 900}, color_scheme="dark")
+ if base.startswith("http://fde.test"):
+ install_static_route(context)
+
+ remote_requests: list[str] = []
+ page = context.new_page()
+ allowed_host = urlparse(base).netloc
+
+ def observe_request(request) -> None:
+ parsed = urlparse(request.url)
+ if parsed.scheme in {"http", "https"} and parsed.netloc != allowed_host:
+ remote_requests.append(request.url)
+
+ page.on("request", observe_request)
+
+ reset(page, base)
+ assert_first_view(page)
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
+
+ # Clear, supportable input returns value immediately instead of starting a questionnaire.
+ clear_input = (
+ "Should we qualify the alternate source or retain the incumbent? "
+ "Schedule risk, resilience, and cost matter. Delay and shortage are possible."
+ )
+ page.locator("#universal-input").fill(clear_input)
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ assert "qualify the alternate source" in page.locator('[data-fde-field="decision"]').inner_text().lower()
+ assert page.get_by_role("button", name="Compare options").count() == 1
+ assert page.get_by_role("button", name="Refine").count() == 1
+ assert page.get_by_role("button", name="Start another").count() == 1
+
+ # Refine returns to the same freeform source context with no new form.
+ page.get_by_role("button", name="Refine").click()
+ assert_first_view(page)
+ assert page.locator("#universal-input").input_value() == clear_input
+ page.locator("#universal-input").fill(clear_input + " Technical performance also matters.")
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ assert "Technical performance" in page.locator('[data-fde-field="what_matters"]').inner_text()
+
+ # Sparse input still yields a partial brief. No compulsory clarification screen appears.
+ page.get_by_role("button", name="Start another").click()
+ page.locator("#universal-input").fill("Qualification evidence is incomplete and the mission dependency is unclear.")
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ assert "not explicit" in page.locator('[data-fde-field="decision"]').inner_text().lower()
+ assert "decision itself" in page.locator('[data-fde-field="uncertainty"]').inner_text().lower()
+
+ # Multiple decisions are preserved rather than forcing a choose-one page.
+ page.get_by_role("button", name="Start another").click()
+ page.locator("#universal-input").fill(
+ "Should we qualify a second source? Should we redesign around the dependency? "
+ "Should we build strategic inventory?"
+ )
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ decision_text = page.locator('[data-fde-field="decision"]').inner_text()
+ assert "qualify a second source" in decision_text.lower()
+ assert "redesign around the dependency" in decision_text.lower()
+ assert "build strategic inventory" in decision_text.lower()
+ assert "3 decision questions preserved" in page.locator(".universal-preserved").inner_text().lower()
+
+ # Large candidate sets remain preserved before bounded formal comparison.
+ page.get_by_role("button", name="Start another").click()
+ page.locator("#universal-input").fill(
+ "Choose between Supplier Alpha, Supplier Bravo, an alternate material, a reserve, or subsystem redesign. "
+ "Cost, schedule, resilience, qualification, and compliance matter. Delay and shortage are possible."
+ )
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ preserved = page.locator(".universal-preserved").inner_text().lower()
+ assert "5 possible choices preserved" in preserved
+ assert "5 criteria preserved" in preserved
+
+ # Formal depth is voluntary and visible on one surface.
+ page.get_by_role("button", name="Compare options").click()
+ assert page.locator("#universal-response-title").inner_text() == "Compare options"
+ assert page.get_by_text("Options", exact=True).count() >= 1
+ assert page.get_by_text("What matters", exact=True).count() >= 1
+ assert page.get_by_text("What may change", exact=True).count() >= 1
+ assert page.get_by_role("button", name="Open Decision Lab").count() == 1
+ # Reduce the preselected choices/criteria to formal limits and provide a second scenario if needed.
+ choice_boxes = page.locator('input[name="formal-choice"]')
+ for index in range(choice_boxes.count()):
+ choice_boxes.nth(index).set_checked(index < 3)
+ goal_boxes = page.locator('input[name="formal-goal"]')
+ for index in range(goal_boxes.count()):
+ goal_boxes.nth(index).set_checked(index < 4)
+ page.locator("#universal-add-futures").fill("Demand changes")
+ page.get_by_role("button", name="Open Decision Lab").click()
+ page.wait_for_url("**#/decision")
+ page.locator("#decision-work").wait_for(state="visible")
+ # Decision Map remains a technical Decision Lab concept, not the first-run surface.
+ assert page.locator("#decision-question").input_value().strip()
+ assert page.locator(".guided-starting-context").count() == 1
+
+ # Saved-work protection: a new brief may exist without overwriting formal work.
+ # Saved-work protection is enforced when a second brief tries to enter Decision Lab.
+ page.goto(base, wait_until="networkidle")
+ page.locator("#universal-input").fill(
+ "Should we qualify another supplier or retain the incumbent? Cost and reliability matter. Delay and shortage are possible."
+ )
+ page.get_by_role("button", name="Continue").click()
+ assert_brief(page)
+ page.get_by_role("button", name="Compare options").click()
+ page.locator("#universal-add-futures").fill("Requirements change")
+ page.get_by_role("button", name="Open Decision Lab").click()
+ assert "saved FDE decision already exists" in page.locator("#universal-response-title").inner_text()
+ assert page.get_by_role("link", name="Open Decision Lab").count() == 1
+
+ # Information requests retain the existing no-fabrication boundary.
+ page.evaluate("localStorage.clear(); sessionStorage.clear()")
+ page.goto(base, wait_until="networkidle")
+ page.locator("#universal-input").fill("What is the current spot price of gallium?")
+ page.get_by_role("button", name="Continue").click()
+ assert "does not retrieve outside facts" in page.locator("#universal-response-title").inner_text().lower()
+
+ # Scope and safety boundaries remain fail-closed before first-run structuring.
+ page.get_by_role("button", name="Adjust").click()
+ page.locator("#universal-input").fill("Should I cut myself or call someone?")
+ page.get_by_role("button", name="Continue").click()
+ assert "outside FDE’s comparison scope" in page.locator("#universal-response-title").inner_text()
+ assert "988" in page.locator(".universal-boundary").inner_text()
+
+ # Over-length input is rejected without silent truncation.
+ page.get_by_role("button", name="Adjust").click()
+ page.locator("#universal-input").fill("x" * 12001)
+ page.get_by_role("button", name="Continue").click()
+ assert page.locator("#universal-input").count() == 1
+ assert "longer than FDE can safely structure" in page.locator("#universal-limit").inner_text()
+
+ # sessionStorage recovery retains in-progress source text; old v1 transient state is ignored.
+ page.locator("#universal-input").fill("Refresh should preserve this source context.")
+ assert page.evaluate(f"Boolean(sessionStorage.getItem('{SESSION_KEY}'))")
+ page.evaluate(f"sessionStorage.setItem('{OLD_SESSION_KEY}', JSON.stringify({{'version': 1, 'view': 'question'}}))")
+ page.reload(wait_until="networkidle")
+ assert page.locator("#universal-input").input_value() == "Refresh should preserve this source context."
+
+ # Mobile/reflow: accepted first view stays operable without page-level horizontal scrolling.
+ mobile = browser.new_context(viewport={"width": 375, "height": 812}, color_scheme="dark")
+ if base.startswith("http://fde.test"):
+ install_static_route(mobile)
+ mobile_page = mobile.new_page()
+ mobile_page.goto(base, wait_until="networkidle")
+ assert_first_view(mobile_page)
+ assert mobile_page.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1")
+ assert mobile_page.get_by_role("button", name="Continue").bounding_box()["height"] >= 44
+ mobile.close()
+
+ # Appearance remains operable and honors the browser color_scheme.
+ light = browser.new_context(viewport={"width": 1280, "height": 900}, color_scheme="light")
+ if base.startswith("http://fde.test"):
+ install_static_route(light)
+ light_page = light.new_page()
+ light_page.goto(base, wait_until="networkidle")
+ assert light_page.locator("html").get_attribute("data-theme") == "light"
+ assert light_page.locator("#theme-toggle").inner_text() == "Appearance"
+ light.close()
+
+ assert not remote_requests, f"FDE made unexpected remote requests: {remote_requests}"
+ context.close()
+ finally:
+ browser.close()
+ finally:
+ server.shutdown()
if __name__ == "__main__":
run()
- print("FIRST-RUN UX E2E PASS")
+ print("FIRST-RUN UX E2E PASS")
\ No newline at end of file
From d58ac807e9550a21fc90abda9b49718095432285 Mon Sep 17 00:00:00 2001
From: Bridge Node 7 <291729790+Bridge-Node-7@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:12:57 -0700
Subject: [PATCH 05/28] Align FDE shell with Bridge Node 7
---
site/index.html | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/site/index.html b/site/index.html
index 6f6b747..0edc22b 100644
--- a/site/index.html
+++ b/site/index.html
@@ -40,19 +40,20 @@
Decision Lab
Governed context
Appearance
+ Partner
-
+ Frontier Decision Engine What are you considering? JavaScript is required for the browser-local decision workspace.
Human authority: FDE structures and calculates; it does not approve or authorize a decision. The public deployment is for public or sanitized material only.
How FDE works