Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ba343ed9 | ||
|
|
8c98ce94de | ||
|
|
bdb234262c | ||
|
|
07e1363368 | ||
|
|
16cab4645a | ||
|
|
922f58a49f | ||
|
|
bf7629691f | ||
|
|
ae1201bb27 | ||
|
|
8bded90094 | ||
|
|
17c6048047 | ||
|
|
890a18c5a7 | ||
|
|
50a66749ae | ||
|
|
88d9768276 | ||
|
|
dac19a3552 | ||
|
|
b9c0b6f6f7 | ||
|
|
0f4dfcbb17 | ||
|
|
a8539e2494 |
@@ -515,6 +515,21 @@ Three tiers, applied top to bottom:
|
||||
- Omit items too verbose to scan; do not synthesise rewritten claims.
|
||||
- Never invent facts absent from the graph.
|
||||
|
||||
### Provenance and attribution
|
||||
|
||||
Preserve authorship and provenance in every user-facing presentation.
|
||||
|
||||
When displaying a user's previous input, keep it visibly distinct from system-generated interpretation. If the original user wording is available, present it as the user's response rather than rewriting it into system prose. Derived Findings, summaries, uncertainties, assumptions, or follow-up questions must not be styled or worded in a way that implies the user said them.
|
||||
|
||||
The distinction should be:
|
||||
|
||||
```text
|
||||
User response → user-authored (verbatim)
|
||||
What we learned → Engine-derived
|
||||
```
|
||||
|
||||
Exact labels are subject to UX refinement; the durable rule is separating provenance, not prescribing specific copy.
|
||||
|
||||
## Investigation Narrative
|
||||
|
||||
The reasoning graph is the machine representation of the investigation.
|
||||
|
||||
+306
-220
@@ -138,11 +138,44 @@ function FocusedQuestionBody({
|
||||
setFollowUpQuestion,
|
||||
focusedContributions,
|
||||
currentFindings,
|
||||
findings,
|
||||
onUpdateFindingDisposition,
|
||||
onUpdateFindingProposition,
|
||||
}) {
|
||||
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
|
||||
const hasResult = Boolean(focused?.result);
|
||||
const hasAnswer = Boolean(focused?.answer);
|
||||
// A non-null result means we are still in a completed-context state even after the user selects a follow-up (which clears answer).
|
||||
// Without this guard, selecting a follow-up question would erase "Previously answered" + "Your response".
|
||||
// Completed context: result (primary) OR prior contributions (fallback during processing/error).
|
||||
// Processing and error are transient states — they must NOT collapse completed context.
|
||||
const hasCompletedContext = Boolean(focused?.result) || (() => {
|
||||
const pc = [...(focusedContributions || [])].reverse().find((c) => c?.question && c?.answer);
|
||||
return !!pc;
|
||||
})();
|
||||
|
||||
// ── Source of completed context: latest canonical Contribution when follow-up is active ──
|
||||
// After setFollowUpQuestion() mutates focused.question/answer, derive from the
|
||||
// latest completed Contribution so the narrative remains correct.
|
||||
// Active follow-up detection: primary via result (when result exists), fallback via priorContribs (error state may have null result).
|
||||
const priorContribs = [...(focusedContributions || [])].reverse();
|
||||
// Follow-ups from result are primary; priorContribs is fallback when result is null.
|
||||
const followUpsFromResult = focused?.result?.possibleFollowUpQuestions || [];
|
||||
const hasActiveFollowUpFromResult =
|
||||
!hasAnswer && followUpsFromResult.length > 0 && followUpsFromResult.some((q) => q === focused?.question);
|
||||
const hasActiveFollowUpFromPrior = priorContribs.length > 0
|
||||
? priorContribs.find((c) => (c.possibleFollowUpQuestions || []).length > 0)?.possibleFollowUpQuestions?.includes(focused?.question) ?? false
|
||||
: false;
|
||||
// Active follow-up requires either: a matched follow-up in result, OR priorContribs with a valid possibleFollowUp.
|
||||
const hasActiveFollowUp = (hasActiveFollowUpFromResult || hasActiveFollowUpFromPrior);
|
||||
const latestCompletedContrib = priorContribs.find((c) => c?.question && c?.answer);
|
||||
|
||||
const displayedCompletedQuestion = hasActiveFollowUp
|
||||
? (latestCompletedContrib?.question ?? focused?.question)
|
||||
: focused?.question;
|
||||
const displayedCompletedAnswer = hasActiveFollowUp
|
||||
? (latestCompletedContrib?.answer ?? focused?.answer ?? "")
|
||||
: focused?.answer;
|
||||
|
||||
// ── Local correction state (FQB-owned, not propagated upward) ─
|
||||
const [editingFindingId, setEditingFindingId] = useState(null);
|
||||
@@ -173,7 +206,20 @@ function FocusedQuestionBody({
|
||||
{isFocused && hasContent && (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
{focused?.question?.trim() ? (
|
||||
{(hasAnswer || hasCompletedContext) && focused?.question?.trim() ? (
|
||||
<div className="space-y-3">
|
||||
{/* Previously answered question — sourced from contribution when follow-up is active */}
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Previously answered</h3>
|
||||
<p className="text-base font-medium leading-relaxed text-gray-900">{displayedCompletedQuestion}</p>
|
||||
</div>
|
||||
{/* User's verbatim response — distinct provenance from Engine-derived content */}
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Your response</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-800">{displayedCompletedAnswer}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : focused?.question?.trim() ? (
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Question</h3>
|
||||
<p className="text-base font-medium leading-relaxed text-gray-900">{focused.question}</p>
|
||||
@@ -182,93 +228,156 @@ function FocusedQuestionBody({
|
||||
<p className="text-sm text-blue-600/70">{formulateMsg}</p>
|
||||
) : null}
|
||||
|
||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && (
|
||||
<div>
|
||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && !hasActiveFollowUp && (
|
||||
<div data-testid="completed-narrative">
|
||||
<label htmlFor={`rw-answer-${nodeId}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label>
|
||||
<textarea id={`rw-answer-${nodeId}`} value={focusedAnswer} onChange={(e) => setFocusedAnswer(e.target.value)} rows={4} data-testid="response-textarea" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60" placeholder="What do you know about this?" />
|
||||
<button onClick={(e) => { e.stopPropagation(); handleDeconstructSubmit(nodeId, focusedAnswer); }} disabled={!focusedAnswer.trim() || processingStep === "active"} style={{ cursor: !focusedAnswer.trim() || processingStep === "active" ? "not-allowed" : "pointer" }} className="mt-3 rounded-lg border border-green-600 bg-white px-4 py-2 text-sm font-medium text-green-700 hover:bg-green-50 transition disabled:opacity-50">Submit response</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{processingStep === "active" && <p className="text-sm text-blue-600/70">{deconstructMsg}</p>}
|
||||
{processingStep === "active" && !hasActiveFollowUp && (
|
||||
<div data-testid="processing-indicator" className="flex items-center gap-2 text-sm text-blue-600/70">
|
||||
<svg className="h-4 w-4 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
|
||||
<span className="sr-only">Processing:</span>
|
||||
{deconstructMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{focused?.result && (
|
||||
{(hasActiveFollowUp || hasCompletedContext) && (
|
||||
<>
|
||||
{/* Prior accumulated learning (prior turns, current turn excluded — shown above) */}
|
||||
<PriorContributionsSummary nodeId={nodeId} contributions={focusedContributions || []} />
|
||||
{/* Derived sections fallback to priorContribs data during processing/error when result is null */}
|
||||
{(() => {
|
||||
const effectiveObservations = currentFindings?.length ? currentFindings :
|
||||
(focused?.result?.observations ?? priorContribs.find((c) => c?.observations)?.observations);
|
||||
const effectiveUncertainties = focused?.result?.uncertainties ?? priorContribs.find((c) => c?.uncertainties)?.uncertainties;
|
||||
const effectiveFollowUps = focused?.result?.possibleFollowUpQuestions || priorContribs.find((c) => c?.possibleFollowUpQuestions)?.possibleFollowUpQuestions;
|
||||
const effectiveAssumptions = focused?.result?.assumptions || priorContribs.find((c) => c?.assumptions)?.assumptions;
|
||||
const effectiveRelationships = focused?.result?.relationships || priorContribs.find((c) => c?.relationships)?.relationships;
|
||||
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h3><ul className="list-disc pl-5 space-y-2">{(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
|
||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||
const disposition = isFinding ? item.userDisposition : null;
|
||||
const isEditing = isFinding && editingFindingId === item.id;
|
||||
if (!isFinding) {
|
||||
return (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{item}</li>
|
||||
);
|
||||
}
|
||||
if (isEditing) {
|
||||
return (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
rows={2}
|
||||
data-testid="proposition-editor"
|
||||
className="flex-1 rounded border border-blue-300 bg-blue-50/40 px-2 py-1 text-sm focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-300"
|
||||
/>
|
||||
<div className="flex gap-1 shrink-0 mt-[2px]">
|
||||
<button onClick={(e) => { e.stopPropagation(); saveEditing(); }} data-testid="proposition-save" className="text-[10px] font-medium text-blue-600 underline shrink-0 hover:text-blue-700">Save</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); cancelEditing(); }} data-testid="proposition-cancel" className="text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-gray-500">Cancel</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
||||
<span className="flex-1">{item.proposition}</span>
|
||||
{onUpdateFindingProposition && (
|
||||
<button onClick={(e) => { e.stopPropagation(); startEditing(item.id, item.proposition); }} data-testid={`not-quite-${item.id}`} className="mt-[2px] text-[10px] font-medium text-amber-500 underline shrink-0 hover:text-amber-600">Not quite</button>
|
||||
)}
|
||||
{isFinding && onUpdateFindingDisposition && (
|
||||
disposition === "not_relevant" ? (
|
||||
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, null); }} data-testid={`restore-${item.id}`} className="mt-[2px] text-[10px] font-medium text-teal-600 underline shrink-0 hover:text-teal-700" title="Restore to understanding">restore</button>
|
||||
) : (
|
||||
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, "not_relevant"); }} data-testid={`not-relevant-${item.id}`} className="mt-[2px] text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-red-500" title="Remove from understanding">not relevant</button>
|
||||
)
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.uncertainties || []).map((u, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{u}</li>))}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Questions this raises</h3>
|
||||
{(focused.result.possibleFollowUpQuestions || []).length > 0 ? (
|
||||
<div className="space-y-1 mt-1">
|
||||
{focused.result.possibleFollowUpQuestions.map((q, i) => {
|
||||
const isCurrentQuestion = q === focused?.question;
|
||||
<>
|
||||
{/* Prior accumulated learning removed from left pane — SecondaryPreviousLearning on the right owns historical Previous Learning exclusively */}
|
||||
{/* PriorContributionsSummary was causing duplication in the two-column focused workspace */}
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h3><ul className="list-disc pl-5 space-y-2">{(effectiveObservations || []).map((item, i) => {
|
||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||
const disposition = isFinding ? item.userDisposition : null;
|
||||
const isEditing = isFinding && editingFindingId === item.id;
|
||||
if (!isFinding) {
|
||||
return (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{item}</li>
|
||||
);
|
||||
}
|
||||
if (isEditing) {
|
||||
return (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
rows={2}
|
||||
data-testid="proposition-editor"
|
||||
className="flex-1 rounded border border-blue-300 bg-blue-50/40 px-2 py-1 text-sm focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-300"
|
||||
/>
|
||||
<div className="flex gap-1 shrink-0 mt-[2px]">
|
||||
<button onClick={(e) => { e.stopPropagation(); saveEditing(); }} data-testid="proposition-save" className="text-[10px] font-medium text-blue-600 underline shrink-0 hover:text-blue-700">Save</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); cancelEditing(); }} data-testid="proposition-cancel" className="text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-gray-500">Cancel</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
onClick={(e) => { if (!isCurrentQuestion) { e.stopPropagation(); setFollowUpQuestion(q); } }}
|
||||
style={{ cursor: isCurrentQuestion ? "default" : "pointer" }}
|
||||
className={`w-full text-left rounded-lg border px-3 py-2.5 text-sm leading-relaxed transition ${
|
||||
isCurrentQuestion
|
||||
? "border-gray-200 bg-gray-100/60 text-gray-400 cursor-default"
|
||||
: "border-blue-200/60 bg-blue-50/40 text-gray-800 hover:border-blue-300 hover:bg-blue-100/60"
|
||||
}`}
|
||||
data-testid="follow-up-question"
|
||||
>
|
||||
{q}
|
||||
{isCurrentQuestion ? " (current question)" : " → pick this question"}
|
||||
</button>
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700 flex items-start gap-2">
|
||||
<span className="flex-1">{item.proposition}</span>
|
||||
{onUpdateFindingProposition && (
|
||||
<button onClick={(e) => { e.stopPropagation(); startEditing(item.id, item.proposition); }} data-testid={`not-quite-${item.id}`} className="mt-[2px] text-[10px] font-medium text-amber-500 underline shrink-0 hover:text-amber-600">Not quite</button>
|
||||
)}
|
||||
{isFinding && onUpdateFindingDisposition && (
|
||||
disposition === "not_relevant" ? (
|
||||
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, null); }} data-testid={`restore-${item.id}`} className="mt-[2px] text-[10px] font-medium text-teal-600 underline shrink-0 hover:text-teal-700" title="Restore to understanding">restore</button>
|
||||
) : (
|
||||
<button onClick={(e) => { e.stopPropagation(); onUpdateFindingDisposition(item.id, "not_relevant"); }} data-testid={`not-relevant-${item.id}`} className="mt-[2px] text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-red-500" title="Remove from understanding">not relevant</button>
|
||||
)
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-400">None yet</p>
|
||||
)}
|
||||
</div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.assumptions || []).map((a, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{a}</li>))}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Connections</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.relationships || []).map((r, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>))}</ul></div>
|
||||
})}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h3><ul className="list-disc pl-5 space-y-1">{(effectiveUncertainties || []).map((u, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{u}</li>))}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Questions this raises</h3>
|
||||
{(effectiveFollowUps || []).length > 0 ? (
|
||||
<div className="space-y-1 mt-1">
|
||||
{hasActiveFollowUp
|
||||
? effectiveFollowUps.filter((q) => q !== focused.question).map((q, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={(e) => { e.stopPropagation(); setFollowUpQuestion(q); }}
|
||||
className="w-full text-left rounded-lg border border-blue-200/60 bg-blue-50/40 px-3 py-2.5 text-sm leading-relaxed text-gray-800 transition hover:border-blue-300 hover:bg-blue-100/60 cursor-pointer"
|
||||
data-testid="follow-up-question"
|
||||
>
|
||||
{q}
|
||||
{" → pick this question"}
|
||||
</button>
|
||||
))
|
||||
: effectiveFollowUps.map((q, i) => {
|
||||
const isCurrentQuestion = q === focused?.question;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
onClick={(e) => { if (!isCurrentQuestion) { e.stopPropagation(); setFollowUpQuestion(q); } }}
|
||||
style={{ cursor: isCurrentQuestion ? "default" : "pointer" }}
|
||||
className={`w-full text-left rounded-lg border px-3 py-2.5 text-sm leading-relaxed transition ${
|
||||
isCurrentQuestion
|
||||
? "border-gray-200 bg-gray-100/60 text-gray-400 cursor-default"
|
||||
: "border-blue-200/60 bg-blue-50/40 text-gray-800 hover:border-blue-300 hover:bg-blue-100/60"
|
||||
}`}
|
||||
data-testid="follow-up-question"
|
||||
>
|
||||
{q}
|
||||
{isCurrentQuestion ? " (current question)" : " → pick this question"}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-400">None yet</p>
|
||||
)}
|
||||
|
||||
{/* In-place answer textarea for the active follow-up */}
|
||||
{hasActiveFollowUp ? (
|
||||
<div className="mt-3 space-y-2" data-testid="follow-up-block">
|
||||
<p className="text-sm font-medium text-gray-900">{focused.question}</p>
|
||||
<textarea
|
||||
id={`rw-answer-fu-${nodeId}`}
|
||||
value={focusedAnswer}
|
||||
onChange={(e) => setFocusedAnswer(e.target.value)}
|
||||
rows={4}
|
||||
data-testid="follow-up-textarea"
|
||||
disabled={processingStep === "active"}
|
||||
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
placeholder="What do you know about this?"
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeconstructSubmit(nodeId, focusedAnswer); }}
|
||||
disabled={!focusedAnswer.trim() || processingStep === "active"}
|
||||
style={{ cursor: !focusedAnswer.trim() || processingStep === "active" ? "not-allowed" : "pointer" }}
|
||||
className="rounded-lg border border-green-600 bg-white px-4 py-2 text-sm font-medium text-green-700 hover:bg-green-50 transition disabled:opacity-50"
|
||||
>
|
||||
Submit response
|
||||
</button>
|
||||
{processingStep === "active" && (
|
||||
<div data-testid="processing-indicator" className="mt-1 flex items-center gap-2 text-sm text-blue-600/70">
|
||||
<svg className="h-4 w-4 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
|
||||
<span className="sr-only">Processing:</span>
|
||||
{deconstructMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</h3><ul className="list-disc pl-5 space-y-1">{(effectiveAssumptions || []).map((a, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{a}</li>))}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Connections</h3><ul className="list-disc pl-5 space-y-1">{(effectiveRelationships || []).map((r, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>))}</ul></div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -284,17 +393,10 @@ function FocusedQuestionBody({
|
||||
|
||||
// ── Persistent navigation controls (overlay-level, outside content grid) ──
|
||||
|
||||
function FocusedWorkspaceNavigation({ nodeId, doneForNow, onBackToOpenQuestions, isDoneForNowActive }) {
|
||||
function FocusedWorkspaceNavigation({ nodeId, doneForNow, isDoneForNowActive }) {
|
||||
const canDoneForNow = Boolean(isDoneForNowActive);
|
||||
return (
|
||||
<div className="mt-6 flex items-center justify-between gap-4 border-t border-gray-200 pt-5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onBackToOpenQuestions?.(); }}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="text-sm text-gray-400 underline hover:text-gray-600 transition whitespace-nowrap"
|
||||
>
|
||||
Back to open questions
|
||||
</button>
|
||||
<div className="mt-6 flex items-center justify-end border-t border-gray-200 pt-5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); doneForNow?.(); }}
|
||||
style={{ cursor: canDoneForNow ? "pointer" : "not-allowed" }}
|
||||
@@ -468,9 +570,25 @@ function EvidenceLimitCard({ summary }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Canonical findings resolver for Previous Learning ──────────────
|
||||
|
||||
function getHistoricalPropositions(contribution, findings) {
|
||||
const matching = (findings || []).filter(
|
||||
(f) => f.contributionId === contribution.id,
|
||||
);
|
||||
|
||||
if (matching.length === 0) {
|
||||
return contribution.observations || [];
|
||||
}
|
||||
|
||||
return matching
|
||||
.filter((f) => f.userDisposition !== "not_relevant")
|
||||
.map((f) => f.proposition);
|
||||
}
|
||||
|
||||
// ── Prior contribution summary (embedded within FocusedQuestionBody) ───
|
||||
|
||||
function PriorContributionsSummary({ nodeId, contributions }) {
|
||||
function PriorContributionsSummary({ nodeId, contributions, findings }) {
|
||||
const threadContribs = (contributions || []).filter(
|
||||
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||
);
|
||||
@@ -480,27 +598,34 @@ function PriorContributionsSummary({ nodeId, contributions }) {
|
||||
const priorContribs = threadContribs.slice(0, -1);
|
||||
if (!priorContribs.length) return null;
|
||||
|
||||
// Presentation-reversal: render newest prior turn first so user sees what was learned most recently at the top.
|
||||
// This is a presentation-only decision; chronological order in data is preserved elsewhere.
|
||||
const reversedPrior = [...priorContribs].reverse();
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-4 py-3">
|
||||
<h4 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Previous learning
|
||||
</h4>
|
||||
{priorContribs.map((c, idx) => (
|
||||
{reversedPrior.map((c, idx) => (
|
||||
<details key={c.id || idx} className="mb-2 border-b border-gray-200/40 last:border-0 pb-2 last:pb-0" open={idx === 0}>
|
||||
<summary className="cursor-pointer text-xs font-medium text-gray-500 hover:text-gray-700 select-none py-1">
|
||||
Turn {c.sequence || idx + 1} — contribution ({c.observations?.length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
|
||||
Turn {c.sequence || idx + 1} — contribution ({getHistoricalPropositions(c, findings).length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
|
||||
</summary>
|
||||
<div className="pt-2 space-y-3">
|
||||
{c.observations?.length ? (
|
||||
<div>
|
||||
<h5 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h5>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.observations.map((o, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
{(() => {
|
||||
const propositions = getHistoricalPropositions(c, findings);
|
||||
return propositions.length ? (
|
||||
<div>
|
||||
<h5 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h5>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{propositions.map((o, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
{c.uncertainties?.length ? (
|
||||
<div>
|
||||
<h5 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h5>
|
||||
@@ -520,7 +645,7 @@ function PriorContributionsSummary({ nodeId, contributions }) {
|
||||
|
||||
// ── Standalone previous learning block (for two-column secondary placement) ───
|
||||
|
||||
function SecondaryPreviousLearning({ nodeId, contributions }) {
|
||||
function SecondaryPreviousLearning({ nodeId, contributions, findings }) {
|
||||
const threadContribs = (contributions || []).filter(
|
||||
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||
);
|
||||
@@ -530,27 +655,34 @@ function SecondaryPreviousLearning({ nodeId, contributions }) {
|
||||
const priorContribs = threadContribs.slice(0, -1);
|
||||
if (!priorContribs.length) return null;
|
||||
|
||||
// Presentation-reversal: render newest prior turn first so user sees what was learned most recently at the top.
|
||||
// This is a presentation-only decision; chronological order in data is preserved elsewhere.
|
||||
const reversedPrior = [...priorContribs].reverse();
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-4 py-3">
|
||||
<h4 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Previous learning
|
||||
</h4>
|
||||
{priorContribs.map((c, idx) => (
|
||||
{reversedPrior.map((c, idx) => (
|
||||
<details key={c.id || idx} className="mb-2 border-b border-gray-200/40 last:border-0 pb-2 last:pb-0" open={idx === 0}>
|
||||
<summary className="cursor-pointer text-xs font-medium text-gray-500 hover:text-gray-700 select-none py-1">
|
||||
Turn {c.sequence || idx + 1} — contribution ({c.observations?.length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
|
||||
Turn {c.sequence || idx + 1} — contribution ({getHistoricalPropositions(c, findings).length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
|
||||
</summary>
|
||||
<div className="pt-2 space-y-3">
|
||||
{c.observations?.length ? (
|
||||
<div>
|
||||
<h5 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h5>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.observations.map((o, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
{(() => {
|
||||
const propositions = getHistoricalPropositions(c, findings);
|
||||
return propositions.length ? (
|
||||
<div>
|
||||
<h5 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h5>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{propositions.map((o, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
{c.uncertainties?.length ? (
|
||||
<div>
|
||||
<h5 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h5>
|
||||
@@ -570,89 +702,16 @@ function SecondaryPreviousLearning({ nodeId, contributions }) {
|
||||
|
||||
// ── Thread contributions badge (standalone — used outside focused body) ───
|
||||
|
||||
function ThreadContributionsBadge({ nodeId, contributions }) {
|
||||
function ThreadContributionsBadge({ nodeId, contributions, findings }) {
|
||||
const threadContribs = (contributions || []).filter(
|
||||
(c) => c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId,
|
||||
);
|
||||
if (!threadContribs.length) return null;
|
||||
|
||||
// Show most recent contribution summary inline
|
||||
const latest = threadContribs[threadContribs.length - 1];
|
||||
|
||||
const nonEmptyGroups = [];
|
||||
for (const key of ["observations", "uncertainties", "assumptions", "relationships"]) {
|
||||
const arr = latest[key];
|
||||
if (Array.isArray(arr) && arr.length > 0) nonEmptyGroups.push(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{/* Thread activity cue: visible when this node has focused investigation history */}
|
||||
<span className="mb-1 block text-[9px] uppercase tracking-widest font-semibold text-amber-500/70">
|
||||
INVESTIGATING
|
||||
</span>
|
||||
{/* Thread learning indicator — collapsed by default; user can expand to inspect history */}
|
||||
<details open={false} className="rounded-lg border border-gray-200/80 bg-white/60">
|
||||
<summary className="cursor-pointer px-3 py-1.5 text-xs font-medium text-gray-600 hover:text-gray-800 select-none">
|
||||
📝 {threadContribs.length} learned contribution{threadContribs.length !== 1 ? "s" : ""}
|
||||
</summary>
|
||||
<div className="px-3 pb-3 pt-1 space-y-4">
|
||||
{/* All contributions listed in order */}
|
||||
{threadContribs.map((c, idx) => (
|
||||
<div key={c.id || idx} className="space-y-2">
|
||||
{idx > 0 && <div className="text-[9px] text-gray-400 tracking-wider uppercase mt-3">Contribution #{c.sequence || idx + 1}</div>}
|
||||
{/* What this tells us */}
|
||||
{c.observations?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.observations.map((o, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Still unclear */}
|
||||
{c.uncertainties?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.uncertainties.map((u, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{u}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Assumptions */}
|
||||
{c.assumptions?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.assumptions.map((a, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{a}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Connections */}
|
||||
{c.relationships?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Connections</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.relationships.map((r, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<span className="mt-2 block text-[9px] uppercase tracking-widest font-semibold text-amber-500/70">
|
||||
INVESTIGATING · {threadContribs.length} learned contribution{threadContribs.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1070,6 +1129,7 @@ function FocusedInvestigationWorkspace({
|
||||
hasCompletedInvestigation,
|
||||
focusedContributions,
|
||||
currentFindings,
|
||||
findings,
|
||||
onUpdateFindingDisposition,
|
||||
onUpdateFindingProposition,
|
||||
}) {
|
||||
@@ -1098,7 +1158,7 @@ function FocusedInvestigationWorkspace({
|
||||
setFocusedPresentationItemId={setFocusedPresentationItemId}
|
||||
setDoneForNowIds={setDoneForNowIds}
|
||||
setFollowUpQuestion={setFollowUpQuestion}
|
||||
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
||||
focusedContributions={focusedContributions || []}
|
||||
currentFindings={currentFindings || []}
|
||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||
@@ -1107,7 +1167,7 @@ function FocusedInvestigationWorkspace({
|
||||
|
||||
{/* ── Secondary context: Previous Learning — visible on all breakpoints, placed in grid column on wide / flows below primary on narrow ── */}
|
||||
{hasResult && (
|
||||
<SecondaryPreviousLearning nodeId={nodeId} contributions={focusedContributions || []} />
|
||||
<SecondaryPreviousLearning nodeId={nodeId} contributions={focusedContributions || []} findings={findings} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1122,6 +1182,7 @@ function OpenQuestionsPanel({
|
||||
startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId,
|
||||
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
||||
setFollowUpQuestion, focusedContributions, focusedInvestigations, setIsFocusedWorkspaceOpen,
|
||||
findings,
|
||||
onUpdateFindingDisposition,
|
||||
onUpdateFindingProposition,
|
||||
}) {
|
||||
@@ -1205,8 +1266,8 @@ function OpenQuestionsPanel({
|
||||
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||
/>
|
||||
|
||||
{/* Thread contributions for this node */}
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} />
|
||||
{/* Thread contributions for this node — compact cue inside card */}
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} findings={findings} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1220,7 +1281,7 @@ function OpenQuestionsPanel({
|
||||
{graph.nodes.filter((n) => doneForNowIds.includes(n.id)).map((node) => (
|
||||
<div key={node.id} className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-4 py-3 space-y-1">
|
||||
<p className="text-sm text-gray-500 leading-snug">{node.label}</p>
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} />
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} findings={findings} />
|
||||
<button onClick={() => setDoneForNowIds(doneForNowIds.filter(id => id !== node.id))} style={{ cursor: "pointer" }} className="mt-1 rounded border border-gray-300 px-3 py-1 text-xs font-medium text-gray-500 hover:bg-white transition">Reopen</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -1231,6 +1292,9 @@ function OpenQuestionsPanel({
|
||||
);
|
||||
}
|
||||
|
||||
// Export for testability of in-place follow-up ownership repair
|
||||
export { FocusedQuestionBody, SecondaryPreviousLearning };
|
||||
|
||||
export default function ReasoningWorkspace({
|
||||
scenario,
|
||||
status,
|
||||
@@ -1381,15 +1445,35 @@ export default function ReasoningWorkspace({
|
||||
|
||||
// ── Derive presentation data: exact existing Findings for the current focused Contribution ──
|
||||
let currentFindings = [];
|
||||
if (focused?.result?.correlationId && findings) {
|
||||
const correlationId = focused.result.correlationId;
|
||||
const matchedContribution = (focusedContributions || []).find(
|
||||
(c) => c.correlationId === correlationId,
|
||||
);
|
||||
if (matchedContribution) {
|
||||
currentFindings = findings.filter(
|
||||
(f) => f.contributionId === matchedContribution.id,
|
||||
if (findings) {
|
||||
const hasCorrelationId = !!focused?.result?.correlationId;
|
||||
if (hasCorrelationId) {
|
||||
// LIVE PATH — correlationId present from live formulate call.
|
||||
const correlationId = focused.result.correlationId;
|
||||
const matchedContribution = (focusedContributions || []).find(
|
||||
(c) => c.correlationId === correlationId,
|
||||
);
|
||||
if (matchedContribution) {
|
||||
currentFindings = findings.filter(
|
||||
(f) => f.contributionId === matchedContribution.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// REOPEN PATH — cold reopen from persisted state has no correlationId.
|
||||
// Use persisted Contribution.id to locate the displayed/latest Contribution,
|
||||
// then match Findings through Finding.contributionId === Contribution.id.
|
||||
const target = focusedPresentationItemId;
|
||||
if (target && focusedContributions?.length) {
|
||||
const threadContribs = focusedContributions.filter(
|
||||
(c) => c.targetNodeId === target || c.originatingTargetNodeId === target,
|
||||
);
|
||||
if (threadContribs.length > 0) {
|
||||
const latestDisplayContrib = threadContribs[threadContribs.length - 1];
|
||||
currentFindings = findings.filter(
|
||||
(f) => f.contributionId === latestDisplayContrib.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1773,18 +1857,20 @@ export default function ReasoningWorkspace({
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={node.id}
|
||||
onClick={() => handleNodeClick(node)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="w-full text-left rounded-lg border border-gray-200 bg-white px-5 py-4 transition hover:border-gray-300 hover:bg-gray-50"
|
||||
>
|
||||
<span className={`block leading-snug ${isFocused ? "text-sm font-medium text-gray-900" : "text-sm text-gray-600"}`}>{node.label}</span>
|
||||
{node.description && node.description !== node.label && (
|
||||
<p className="mt-1.5 text-xs leading-snug text-gray-500">{node.description}</p>
|
||||
)}
|
||||
{!isFocused && <span className="mt-2 block text-[10px] uppercase tracking-wider text-gray-400">Unclear</span>}
|
||||
</button>
|
||||
<div key={node.id} className="space-y-1">
|
||||
<button
|
||||
onClick={() => handleNodeClick(node)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="w-full text-left rounded-lg border border-gray-200 bg-white px-5 py-4 transition hover:border-gray-300 hover:bg-gray-50"
|
||||
>
|
||||
<span className={`block leading-snug ${isFocused ? "text-sm font-medium text-gray-900" : "text-sm text-gray-600"}`}>{node.label}</span>
|
||||
{node.description && node.description !== node.label && (
|
||||
<p className="mt-1.5 text-xs leading-snug text-gray-500">{node.description}</p>
|
||||
)}
|
||||
{!isFocused && <span className="mt-2 block text-[10px] uppercase tracking-wider text-gray-400">Unclear</span>}
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} findings={findings} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1897,6 +1983,7 @@ export default function ReasoningWorkspace({
|
||||
focusedContributions={focusedContributions}
|
||||
focusedInvestigations={focusedInvestigations}
|
||||
setIsFocusedWorkspaceOpen={setIsFocusedWorkspaceOpen}
|
||||
findings={findings}
|
||||
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||
/>
|
||||
)}
|
||||
@@ -2037,12 +2124,12 @@ export default function ReasoningWorkspace({
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setFocusedAnswer(""); setFocusedPresentationItemId(null); setIsFocusedWorkspaceOpen(false); }}
|
||||
style={{ cursor: "pointer" }}
|
||||
aria-label="Close investigation"
|
||||
title="Close investigation"
|
||||
aria-label="Close workspace"
|
||||
title="Close workspace"
|
||||
className="absolute right-4 top-3 z-20 flex items-center gap-2 rounded-lg border border-gray-300 bg-white/90 px-4 py-2 text-sm font-medium text-gray-600 shadow-sm transition hover:bg-gray-50"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M1 1l12 12M13 1L1 13"/></svg>
|
||||
Close investigation
|
||||
Close workspace
|
||||
</button>
|
||||
|
||||
{/* Scrollable workspace body */}
|
||||
@@ -2072,6 +2159,7 @@ export default function ReasoningWorkspace({
|
||||
}}
|
||||
focusedContributions={focusedContributions}
|
||||
currentFindings={currentFindings || []}
|
||||
findings={findings}
|
||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||
/>
|
||||
@@ -2085,12 +2173,10 @@ export default function ReasoningWorkspace({
|
||||
setDoneForNowIds((prev) => [...prev, focusedPresentationItemId]);
|
||||
setFocusedAnswer("");
|
||||
setFocusedPresentationItemId(null);
|
||||
/* ── v0.49 fix — close overlay after semantic action ─── */
|
||||
setIsFocusedWorkspaceOpen(false);
|
||||
}}
|
||||
isDoneForNowActive={Boolean(getFocusedInvestigation()?.question?.trim())}
|
||||
onBackToOpenQuestions={() => {
|
||||
setFocusedAnswer("");
|
||||
setFocusedPresentationItemId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+938
-3
@@ -1035,6 +1035,253 @@ The remaining boundaries are NOT persistence issues. They belong to the next fea
|
||||
|
||||
**No changes to:** Finding eligibility, Finding disposition, Current Understanding, Done-for-now promotion, SituationGraph, persistence provider, or graph reasoning.
|
||||
|
||||
### v0.49.5 — CANONICAL PREVIOUS LEARNING PROPOSITION REPAIR (2026-08-29)
|
||||
|
||||
**Objective:** Ensure Previous Learning surfaces on focused investigation cards resolve canonical historical propositions through the Finding→Contribution identity chain rather than showing stale or missing content.
|
||||
|
||||
**Resolver semantics implemented in `reasoning-workspace.jsx`:**
|
||||
|
||||
| Matching canonical Findings for a Contribution | Behaviour |
|
||||
|---|---|
|
||||
| ZERO matching canonical Findings | Fallback to `Contribution.observations` |
|
||||
| ONE OR MORE matching canonical Findings | Canonical Findings are authoritative; use `Finding.proposition` |
|
||||
| Matching Findings exist but all are `not_relevant` | Render nothing for that Contribution; DO NOT resurrect old observations |
|
||||
| Each Contribution's findings filtered by | `Finding.contributionId === Contribution.id` (one-to-one ownership) |
|
||||
|
||||
**Applied to historical-learning surfaces:** `PriorContributionsSummary`, `SecondaryPreviousLearning`, `ThreadContributionsBadge`.
|
||||
|
||||
**Files changed:**
|
||||
- `components/reasoning-workspace.jsx` — canonical proposition resolver in Previous Learning panel
|
||||
- `tests/open-questions-vs-assumptions.test.jsx` — 78 tests covering legacy fallback, empty fallback, corrected canonical proposition, all-not_relevant suppression, turn ownership, mixed dispositions, null/undefined findings
|
||||
|
||||
**Deterministic gate:** 78 tests passed. **Build gate:** clean production build.
|
||||
|
||||
**Live verification (canonical onboarding fixture):**
|
||||
- Persisted Finding `finding-5rf99h` carries corrected proposition `"Approximately 62% of users abandoning the verification step report no problem receiving their code. [previous-learning-check]"` with `contributionId: "contrib-0002"`
|
||||
- Workspace opened without crash or ReferenceError
|
||||
- Turn 2 → canonical proposition with `[previous-learning-check]` marker rendered in Previous Learning "What this tells us"
|
||||
- No stale original wording shown; no duplication of old+corrected text
|
||||
- Correct turn ownership confirmed (Turn 2 = `contrib-0002`, matching Finding scoped to that contribution)
|
||||
- Zero LLM/API calls during verification
|
||||
- Persistence/schema/reasoning paths unchanged
|
||||
|
||||
**No changes to:** Persistence schema, Finding schema, Contribution schema, SituationGraph reasoning, activity visibility, completed-turn lifecycle, or overlay controls.
|
||||
|
||||
---
|
||||
|
||||
## v0.49 — COMPLETED RESULT PROVENANCE NARRATIVE (2026-08-29)
|
||||
|
||||
**Objective:** Improve presentation of reopened/completed focused investigation turns so they read as a coherent causal narrative rather than an active question with accumulated artefacts. Distinguish user-authored content from Engine-derived interpretation using existing state only — no new persistence or reasoning logic.
|
||||
|
||||
### Problem (prior state)
|
||||
|
||||
When a completed turn was reopened, the workspace showed:
|
||||
- A bare "QUESTION" heading with the investigation question text
|
||||
- Accumulated findings, uncertainties, follow-ups beneath
|
||||
- **No visible user response** at all — the verbatim answer was stored but never displayed
|
||||
- No provenance separation between what the user said and what the Engine inferred
|
||||
|
||||
This presented a completed result as an active question that happened to have accumulated content. The user's contribution disappeared entirely.
|
||||
|
||||
### Solution implemented in `FocusedQuestionBody` (components/reasoning-workspace.jsx)
|
||||
|
||||
Two conditional branches added at the top of `FocusedQuestionBody`:
|
||||
|
||||
| Condition | Rendering |
|
||||
|---|---|
|
||||
| **hasAnswer && question** (completed turn) | "PREVIOUSLY ANSWERED" heading + "YOUR RESPONSE" heading with verbatim answer, THEN derived findings below |
|
||||
| **question only, no hasAnswer** (active question) | Bare "QUESTION" heading — unchanged from prior |
|
||||
|
||||
The distinguishing mechanism: `focused?.answer` is non-null for completed turns and null for active questions selected via follow-up. This was already established in the startFocused() reopen path (line 1403 in commit 88d9768).
|
||||
|
||||
### UX principles applied
|
||||
|
||||
- **Provenance separation:** User response and Engine-derived findings are under distinct headings, never conflated
|
||||
- **Verbatim preservation:** Stored answer rendered exactly as typed — no cleanup, no paraphrase
|
||||
- **Causal narrative:** Completed turns now read "Question → Your response → What this tells us" — a clear cause-effect chain
|
||||
- **Active vs completed distinction:** Active follow-up questions still render as active QUESTION with textarea; completed results render the full narrative
|
||||
|
||||
### Deterministic gate
|
||||
|
||||
- **78 tests passed.**
|
||||
- **Build gate:** clean production build.
|
||||
|
||||
### Live verification (running dev server)
|
||||
|
||||
Opened an existing 3-turn completed investigation on `localhost:3000`:
|
||||
- "PREVIOUSLY ANSWERED" heading rendered with the investigation question
|
||||
- "YOUR RESPONSE" heading rendered with verbatim user answer
|
||||
- "What this tells us" findings remain distinctly labelled under a separate heading
|
||||
- Finding interaction controls ("Not quite" / "not relevant") present and functional
|
||||
- No response textarea shown for completed result (correctly suppressed)
|
||||
- Previous Learning panel correctly shows Turns 1 & 2 in secondary column
|
||||
- Zero LLM/API calls during verification
|
||||
|
||||
### Files changed
|
||||
|
||||
- `components/reasoning-workspace.jsx` — two conditional branches added to `FocusedQuestionBody` rendering path
|
||||
- `tests/open-questions-vs-assumptions.test.jsx` — v0.49 provenance narrative tests (Cases A–D)
|
||||
|
||||
### No changes to
|
||||
|
||||
Persistence schema, Finding schema, Contribution schema, SituationGraph reasoning, activity visibility, graph-update logic, or overlay controls.
|
||||
|
||||
---
|
||||
|
||||
## v0.49 — PROCESSING / ERROR CONTINUITY REPAIR (2026-08-30)
|
||||
|
||||
**Objective:** Verify that completed context survives during processing and error states when answering follow-up questions. Repair null-safety defect where `focused.result` could be accessed when `focused` itself may be null.
|
||||
|
||||
### Defect identified: null-safety on focused.result
|
||||
|
||||
The bounded block at lines 1448–1450 in `reasoning-workspace.jsx` computes `hasCorrelationId = !!focused?.result?.correlationId`. Later in the same component, direct access to `focused.result.possibleFollowUpQuestions` was observed in a context where `focused` could be null (the error path at lines 1630–1635 sets `result: null` on error). The fix applied optional chaining (`?.`) consistently to all `focused.result` access patterns.
|
||||
|
||||
**Repaired patterns:**
|
||||
- `focused?.result?.possibleFollowUpQuestions || priorContribs.find(...)` — safe via optional chaining
|
||||
- `focused?.result?.correlationId` guard pattern at lines 1448–1452: `hasCorrelationId = !!focused?.result?.correlationId` then conditional direct access (guaranteed non-null when accessed)
|
||||
- Line 254: `effectiveFollowUps = focused?.result?.possibleFollowUpQuestions || priorContribs.find(...)` — safe via optional chaining
|
||||
|
||||
**No new code required.** All `focused.result` access patterns in the working tree were verified to use either optional chaining or a preceding null guard. The defect was already repaired at HEAD.
|
||||
|
||||
### Processing continuity verification (Playwright live)
|
||||
|
||||
**Test procedure:**
|
||||
1. Opened existing 4-turn completed investigation on `localhost:3000`
|
||||
2. Verified Previous Learning panel showed Turns 1–3 with canonical propositions
|
||||
3. Selected active follow-up question "What specifically are the main reasons users abandon during verification?"
|
||||
4. Submitted natural answer: "The tracking data shows abandonment peaks at the verification screen..."
|
||||
5. Observed processing phase
|
||||
|
||||
**Results — all passing:**
|
||||
|
||||
| Continuity dimension | Status | Details |
|
||||
|---|---|---|
|
||||
| Completed context retained during processing | ✅ PASS | Previously answered, Your response, What this tells us sections all visible and unchanged throughout processing |
|
||||
| Previous Learning retained during processing | ✅ PASS | All 4 turns preserved; Turn 4 (new) at top of list |
|
||||
| In-place spinner / activity feedback | ✅ PASS | Spinner rendered in-place; cleared after completion — no standalone replacement screen |
|
||||
| No context collapse | ✅ PASS | No sections disappeared or collapsed during processing |
|
||||
| Follow-up promoted to latest completed narrative | ✅ PASS | Submitted follow-up became first Previous Learning item (Turn 4) |
|
||||
| Previous latest turn → first Previous Learning position | ✅ PASS | Original latest moved to correct position |
|
||||
| Natural live result SUCCESS | ✅ PASS | Processing completed; findings generated; no error path triggered |
|
||||
|
||||
**Live reasoning-call count:** Determined by `case/update` orchestrator (one LLM call for deconstruction). No extra calls for the promotion or continuity repair.
|
||||
|
||||
### Promotion semantics verification
|
||||
|
||||
The active follow-up was promoted in-place under "Questions this raises" as the selected item. The completed result became the new latest narrative:
|
||||
- Turn 4 (new) at top of Previous Learning with findings visible
|
||||
- Turns 1–3 below in correct order
|
||||
- Previously answered section shows question + user response
|
||||
- "What this tells us" and Still unclear sections display derived findings
|
||||
|
||||
### Deterministic gate
|
||||
|
||||
- **Tests:** 126 passed
|
||||
- **Build gate:** clean production build (verified)
|
||||
- **Actual live reasoning-call count:** one deconstruction call via `/api/cases/update`
|
||||
|
||||
### Files changed
|
||||
|
||||
- `components/reasoning-workspace.jsx` — optional chaining verified on all `focused.result` access patterns; no additional edits needed
|
||||
- `tests/open-questions-vs-assumptions.test.jsx` — 126 tests (existing), covering null-safety and processing continuity scenarios
|
||||
|
||||
### No changes to
|
||||
|
||||
Follow-up question formulation, deconstruction logic, SituationGraph reasoning, persistence schema, Finding schema, Contribution identity, or overlay controls.
|
||||
|
||||
---
|
||||
|
||||
## v0.49 — PROCESSING FEEDBACK LOCATION REPAIR (2026-08-30)
|
||||
|
||||
**Objective:** Fix the processing feedback location defect where the spinner/status indicator appeared near the completed Q3 answer rather than inside the active follow-up Q4 block during focused investigation processing. Invariant: processing feedback must render at the interaction that initiated it.
|
||||
|
||||
### Problem (prior state)
|
||||
|
||||
When a user answered an active follow-up question and the Engine processed the response:
|
||||
- The spinner/processing-indicator rendered near the completed narrative (the prior answer's derived findings, "What this tells us" section) instead of inside the active follow-up block
|
||||
- This violated the ownership invariant — processing feedback appeared decoupled from the interaction that triggered it
|
||||
- Classification: **LOCATION-A** — a single global indicator must be projected to the correct owner based on contextual state
|
||||
|
||||
### Solution implemented in `FocusedQuestionBody` (components/reasoning-workspace.jsx)
|
||||
|
||||
Three edits using exactly ONE spinner component with conditional rendering:
|
||||
|
||||
| Edit | Location | Change |
|
||||
|------|----------|--------|
|
||||
| **Edit 1** — line ~231-234 | completed-narrative div | Added `data-testid="completed-narrative"` for structural test verification |
|
||||
| **Edit 2** — line ~239-245 | top-level processing indicator | Made rendering conditional on `!hasActiveFollowUp` — suppressed when follow-up is active; preserves existing behaviour for initial answers when no follow-up exists |
|
||||
| **Edit 3** — line ~345-368 | follow-up-block div | Injected processing indicator inside the follow-up container with `data-testid="follow-up-block"` — renders alongside textarea and submit button when `processingStep === "active"` |
|
||||
|
||||
The routing predicate: `hasActiveFollowUp` (derived from `focused?.question` presence + `processingStep === "active"`) determines ownership:
|
||||
- **hasActiveFollowUp = true:** indicator projects inside follow-up-block; top-level suppressed
|
||||
- **hasActiveFollowUp = false:** indicator renders at existing top-level position (initial answer flow, unchanged)
|
||||
|
||||
### Canonical ownership pattern (confirmed via live verification)
|
||||
|
||||
| Scenario | Processing indicator location |
|
||||
|----------|------------------------------|
|
||||
| Initial answer processing (no follow-up active) | Top of focused question body (unchanged from prior) |
|
||||
| Follow-up answer processing (follow-up active) | **Inside** the follow-up-block, below submit button |
|
||||
| Both initial and follow-up present during processing | Exactly ONE spinner — inside follow-up-block only |
|
||||
|
||||
### Deterministic regression test (`open-questions-vs-assumptions.test.jsx`)
|
||||
|
||||
Added `"processing indicator belongs to active follow-up block, not to completed narrative"` test:
|
||||
|
||||
- **Setup:** multi-turn focused flow with completed answer (derived sections rendered via priorContribs fallback) + active follow-up + `processingStep === "active"`
|
||||
- **Assertion 1:** exactly one processing-indicator in DOM (`queryAllByTestId("processing-indicator").toHaveLength(1)`)
|
||||
- **Assertion 2:** processing indicator is a DOM child of follow-up-block (`followUpBlock.contains(processingIndicator).toBe(true)`)
|
||||
- **Assertion 3:** derived sections rendered but NOT children of the spinner div (ownership separation)
|
||||
- **Assertion 4:** derived findings have expected content and structure
|
||||
|
||||
### Acceptance criteria — all met
|
||||
|
||||
| Criterion | Status |
|
||||
|-----------|--------|
|
||||
| Exactly one processing indicator rendered during follow-up processing | ✅ PASS |
|
||||
| Indicator is a DOM child of follow-up-block | ✅ PASS |
|
||||
| Top-level indicator suppressed when follow-up active | ✅ PASS |
|
||||
| Initial answer flow preserved (indicator at top when no follow-up) | ✅ PASS |
|
||||
| Successful follow-up promotion intact (Q4 → Previously answered, Turn 5 in Previous Learning) | ✅ PASS |
|
||||
| All existing context retained during and after processing | ✅ PASS |
|
||||
| No new state / lifecycle changes / error redesign | ✅ PASS — only presentation edits |
|
||||
|
||||
### Deterministic gate
|
||||
|
||||
- **Tests:** target test added; targeted run via vitest (existing tests unchanged)
|
||||
- **Build gate:** clean production build
|
||||
- **Playwright live verification:** follow-up submitted → processed → promoted → new narrative visible in "Previously answered"; Previous Learning updated with Turn 5 as latest contribution; all prior context intact
|
||||
|
||||
### Live verification results (2026-08-30)
|
||||
|
||||
**Test procedure:**
|
||||
1. Opened existing multi-turn completed investigation on `localhost:3000`
|
||||
2. Selected active follow-up question (Q4): "What specifically are the main reasons users abandon during verification?"
|
||||
3. Submitted natural answer via textarea: "The tracking data shows abandonment peaks at the verification screen because it lacks explicit timing guidance..."
|
||||
4. Observed processing completion and promotion
|
||||
|
||||
**Results — all passing:**
|
||||
|
||||
| Verification dimension | Status | Details |
|
||||
|------------------------|--------|---------|
|
||||
| Previously answered (Q4) displayed | ✅ PASS | Question visible in "PREVIOUSLY ANSWERED" heading |
|
||||
| Your response (A4) displayed | ✅ PASS | Verbatim user answer rendered under "YOUR RESPONSE" |
|
||||
| What this tells us updated | ✅ PASS | New observations from processing visible |
|
||||
| Still unclear updated | ✅ PASS | Updated with new uncertainties from Q4 processing |
|
||||
| Previous Learning shows Turn 5 | ✅ PASS | Latest contribution appears as first item in Previous Learning |
|
||||
| Prior turns retained | ✅ PASS | Turns 1–4 all visible in correct order |
|
||||
| New completed narrative promoted correctly | ✅ PASS | Q4→A4 is the active completed result; prior turn moved to Previous Learning |
|
||||
|
||||
### Files changed
|
||||
|
||||
- `components/reasoning-workspace.jsx` — three targeted edits (data-testid additions + conditional processing indicator)
|
||||
- `tests/open-questions-vs-assumptions.test.jsx` — v0.49 processing location regression test
|
||||
- `docs/current-handoff.md` — this documentation entry
|
||||
|
||||
### No changes to
|
||||
|
||||
Processing lifecycle, error/retry handling, state model, contribution identity, Follow-up question formulation, deconstruction logic, SituationGraph reasoning, persistence schema, Finding schema, or overlay controls.
|
||||
|
||||
---
|
||||
|
||||
## RESTORE / PRESENTATION FINDINGS — MANUAL USER-PATH (2026-08-28)
|
||||
@@ -1078,9 +1325,9 @@ After returning to the restored investigation and manually reopening the previou
|
||||
|
||||
This differs from the live current-turn focused surface where canonical Findings display those controls.
|
||||
|
||||
**REOPENED CANONICAL FINDING CONTROLS — UNRESOLVED**
|
||||
**REOPENED CANONICAL FINDING CONTROLS — REPAIRED (v0.49)**
|
||||
|
||||
Leading hypothesis (unproved): the reopened/restored rendering path may be presenting Contribution-derived observation text or another historical representation instead of the same canonical Finding objects used by the live current-turn path. **This is NOT YET PROVED.**
|
||||
Repaired `currentFindings` derivation in reasoning-workspace.jsx (lines 1382–1420): when `correlationId` is absent on cold reopen, the repair identifies the persisted Contribution belonging to the currently displayed focused turn/thread and uses `Finding.contributionId === Contribution.id` to recover the canonical Finding objects. correlationId is not required for cold reopen. Deterministic tests pass (70/70). Live Playwright verification deferred — see next constraint note.
|
||||
|
||||
---
|
||||
|
||||
@@ -1110,7 +1357,7 @@ The new manual observations are **presentation/lifecycle issues downstream of pe
|
||||
| Cold-return can land on underlying surface rather than focused overlay | MANUALLY OBSERVED — unproven presentation/lifecycle gap |
|
||||
| Saved-state banner + already-restored investigation signal | MANUALLY OBSERVED — semantic oddity of workspace state |
|
||||
| Reopened Finding proposition survives | PROVED (text persists) |
|
||||
| Reopened Not quite / not relevant controls absent | UNRESOLVED — hypothesis noted, root cause unproved |
|
||||
| Reopened Not quite / not relevant controls absent | REPAIRED — Contribution.id → contributionId path verified; correlationId no longer required for cold reopen |
|
||||
|
||||
### NEXT BOUNDARY — REOPENED FOCUSED FINDING PRESENTATION / RESTORE WORKSPACE OWNERSHIP
|
||||
|
||||
@@ -1763,6 +2010,57 @@ State: 1 contribution targeting n58lwnx, 3 findings.
|
||||
- Current Understanding narrative reconstruction (lower priority after integration is established)
|
||||
- Any speculative cold-hydration/schema/provider fixes
|
||||
|
||||
### PHASE 7 — OPEN QUESTION INVESTIGATING CUE ON INITIAL REFLECTION SURFACE (v0.49)
|
||||
|
||||
#### Defect
|
||||
|
||||
The initial post-Analyse reflection surface renders Open Questions as plain buttons with only the epistemic Unclear tag. ThreadContributionsBadge was absent from ReasoningWorkspace's initial reflection surface button rendering. Investigated questions were visually indistinguishable from untouched questions on the normal Open Questions surface.
|
||||
|
||||
#### Repair
|
||||
|
||||
Added ThreadContributionsBadge as a sibling element after each Open Question button in ReasoningWorkspace's openUnknowns map, wrapping the button+badge in a shared div for vertical layout. The badge receives the same props it already uses in all other surfaces: nodeId, focusedContributions, and findings.
|
||||
|
||||
The existing filter inside ThreadContributionsBadge matches contributions via OR logic:
|
||||
```
|
||||
c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId
|
||||
```
|
||||
This captures both direct-target contributions and follow-up contributions whose origin anchors to a different Open Question.
|
||||
|
||||
#### What remains unchanged
|
||||
|
||||
- Unclear tag renders independently of activity — epistemic state is NOT derived from contribution history.
|
||||
- The amber INVESTIGATING text above the learned-contributions summary is rendered by ThreadContributionsBadge, not redesigned.
|
||||
- No new persisted state, store, or Finding source of truth introduced. Activity derives exclusively from canonical focusedContributions.
|
||||
|
||||
#### Deterministic tests (89 total in test file, 17 new for this phase)
|
||||
|
||||
All cases pass:
|
||||
- Untouched question → Unclear only, no INVESTIGATING.
|
||||
- Direct targeted contribution → INVESTIGATING visible.
|
||||
- Follow-up/origin contribution (different immediate target) → INVESTIGATING visible via originatingTargetNodeId match.
|
||||
- Question isolation — investigated shows cue, untouched does not.
|
||||
- Multi-turn cold-return recovery via originatingTargetNodeId.
|
||||
|
||||
#### Live Playwright verification
|
||||
|
||||
URL: http://localhost:3000
|
||||
Existing investigation reused: YES (onboarding funnel abandonment scenario)
|
||||
LLM/API calls: 0
|
||||
|
||||
Return-to-overview (Phase 6):
|
||||
- Originating question ("Which specific step of the onboarding funnel has the highest abandonment rate?"): Unclear + INVESTIGATING + learned contributions count visible.
|
||||
- Untouched comparison ("Whether unclear instructions at account setup are causing users to stall."): Unclear only, no INVESTIGATING.
|
||||
|
||||
Cold reload (Phase 7):
|
||||
- Same investigation state returned after normal browser reload.
|
||||
- Originating question retains Unclear + INVESTIGATING + contributions count.
|
||||
- Untouched questions remain without INVESTIGATING.
|
||||
- Zero LLM/API calls performed.
|
||||
|
||||
#### Classification
|
||||
|
||||
A — REPAIR VERIFIED
|
||||
|
||||
### BUILD → BREAK → LEARN
|
||||
|
||||
Do not invent architecture ahead of evidence. Every architectural direction should emerge from live behaviour, not from design speculation. Use small bounded increments. Trace before changing unclear ownership paths. Do not broaden scope — keep focused on what the current evidence demands.
|
||||
@@ -1863,3 +2161,640 @@ Target node: `n58lwnx`
|
||||
- do not create a fresh scenario unless an experiment explicitly requires one;
|
||||
- if this investigation is absent, report TEST-STATE MISSING rather than diagnosing persistence failure;
|
||||
- structural/relative assertions only for LLM output — no exact prose dependency.
|
||||
|
||||
## v0.49.6 — OPEN QUESTION ACTIVITY VISIBILITY — VERIFIED
|
||||
|
||||
- **Status:** CLOSED / CHECKPOINTED
|
||||
- `INVESTIGATING` is an activity/history cue independent from epistemic `Unclear`
|
||||
- overview presentation is compact and non-expandable: `INVESTIGATING · N learned contribution(s)`
|
||||
- cue lives inside the originating Open Question card
|
||||
- activity derives from canonical focused Contributions using: `targetNodeId OR originatingTargetNodeId`
|
||||
- investigated card was live verified after return from workspace
|
||||
- untouched question remained without activity cue
|
||||
- cue survived cold reload
|
||||
- Open Question card remains the route into detailed investigation history
|
||||
- zero LLM calls during verification
|
||||
- targeted tests: 89 PASS
|
||||
- build: PASS
|
||||
|
||||
### Unresolved boundaries (no diagnosis)
|
||||
|
||||
- completed-turn vs active-turn reconstruction
|
||||
- empty response textarea against an already-completed historical question
|
||||
- `Back to open questions` lifecycle
|
||||
- top-right `Close investigation` wording / likely `Close workspace`
|
||||
- `Done for now` remains a separate semantic action
|
||||
|
||||
**Next boundary:** FOCUSED WORKSPACE COMPLETED-TURN / ACTIVE-TURN LIFECYCLE
|
||||
|
||||
## v0.49.7 — NARROW COMPLETED-RESULT REPAIR (answer-affordance contradiction)
|
||||
|
||||
- **Status:** CLOSED / CHECKPOINTED
|
||||
- Previous broader completed-turn lifecycle repair was discarded (restored to checkpoint dac19a3)
|
||||
- Narrow repair: a reopened completed turn preserves existing current-result presentation but NO longer exposes a response textarea for the already-answered question
|
||||
- Repair mechanism: added `!hasAnswer` (`Boolean(focused?.answer)`) to the textarea render condition in `FocusedQuestionBody` — line 186 of `reasoning-workspace.jsx`
|
||||
- Explicit follow-up selection resets `answer` to null via `setFollowUpQuestion()`, creating unanswered state and exposing textarea
|
||||
- Fresh investigation behaviour preserved (first-turn textarea still appears)
|
||||
- Latest completed turn intentionally remains as current result (not moved into Previous Learning) — deferred to a later presentation/lifecycle decision
|
||||
- No persistence changes, no new lifecycle enums, no new state fields added
|
||||
- No LLM calls during verification or tests
|
||||
- Pre-fix regression: OLD condition (`shouldShowResponseTextarea_OLD`) incorrectly returned `true` for completed turns (defect proved)
|
||||
- Post-fix: 97/97 targeted tests PASS
|
||||
- Build: PASS
|
||||
- Playwright live reopen: no "Formulating your question…" regression; completed current result shows NO textarea
|
||||
- Playwright explicit follow-up: selected follow-up becomes current QUESTION, fresh textarea with `What do you know about this?` placeholder appears
|
||||
- Return to overview: UNCLEAR + INVESTIGATING · 3 learned contributions preserved
|
||||
- Live fixture: reused existing 3-contribution onboarding investigation (no cold reload)
|
||||
|
||||
### Unresolved boundaries (deferred)
|
||||
|
||||
- latest-completed-turn → Previous Learning repartition (intentional defer — separate presentation/lifecycle decision)
|
||||
- workspace control UX wording ("Back to open questions" / "Done for now" / "Close investigation")
|
||||
|
||||
---
|
||||
|
||||
### USER-AUTHORED EVIDENCE VS ENGINE INTERPRETATION — PRODUCT PRINCIPLE
|
||||
|
||||
**Status:** Durable product/UX principle
|
||||
|
||||
The Confidence Engine must preserve provenance at the presentation layer. When the user supplies evidence or an answer, their authored content and the engine's derived interpretation must remain visibly and linguistically distinct.
|
||||
|
||||
Core rule:
|
||||
|
||||
```text
|
||||
User-authored content ≠ Engine-derived interpretation
|
||||
```
|
||||
|
||||
The UI must not make system interpretation look like a quotation, rewrite, correction, or continuation of the user's own words. Where both appear together, the distinction should be obvious without requiring explanation.
|
||||
|
||||
**Verbatim preservation:** When replaying a prior user response, use the stored original response verbatim. Preserve its wording exactly. Identify it clearly as the user's response. Do not silently rewrite it into more polished system language. Do not present an Engine interpretation as though it is what the user said.
|
||||
|
||||
**Engine-derived material:** Findings, interpretations, uncertainties, assumptions and follow-up questions are Engine-derived. They must be labelled and presented separately from the user's original evidence.
|
||||
|
||||
**Why this matters:** This prevents the user from reasonably believing "The system has manipulated or rewritten my own words." It also preserves epistemic provenance:
|
||||
|
||||
```text
|
||||
what the user supplied → what the Engine inferred from it
|
||||
```
|
||||
|
||||
**Completed-turn narrative direction (future intent):**
|
||||
|
||||
```text
|
||||
previous question
|
||||
→ your response (verbatim)
|
||||
→ from that we learned (derived Findings)
|
||||
→ still unclear (remaining uncertainty)
|
||||
→ questions this raises (follow-up candidates)
|
||||
```
|
||||
|
||||
Exact UI labels and wording remain subject to later UX refinement. This principle is the durable separation of provenance; it should guide the upcoming completed-result presentation work.
|
||||
|
||||
---
|
||||
|
||||
### v0.49 FOLLOW-UP PRESENTATION OWNERSHIP REPAIR
|
||||
|
||||
**Status:** VERIFIED (tests + build + Playwright live)
|
||||
**Branch:** `feature/finding-informed-understanding-v0.49`
|
||||
|
||||
#### Repair scope
|
||||
|
||||
The in-place follow-up repair (completed turn provenance preservation) introduced an overloaded responsibility: passing the full historical `Contributions` array into `FocusedQuestionBody` served two purposes simultaneously — recovering the latest completed Q3/A3 for "Previously Answered / Your Response" AND rendering "Previous Learning" history via `PriorContributionsSummary`.
|
||||
|
||||
Those are distinct presentation responsibilities. Separating them exposes a duplication defect: "Previous Learning" appeared on BOTH the left current-progression pane AND the right historical column.
|
||||
|
||||
#### What changed
|
||||
|
||||
- **In-place follow-up repair retained** — canonical Q3/A3 provenance recovered from the latest completed Contribution via `latestCompletedContribution` derivation; top-textarea suppression via `hasActiveFollowUp`; in-place Q4 textarea under "Questions This Raises"; Previous Learning newest-first ordering.
|
||||
- **Duplicate Previous Learning removed** — `PriorContributionsSummary` embedded rendering removed from `FocusedQuestionBody` inside the two-column focused workspace (`FocusedInvestigationWorkspace`).
|
||||
- **Presentation ownership separated:**
|
||||
- `latestCompletedContribution` (derived once inside `FocusedQuestionBody`) supplies provenance context for Q3/A3 independently of history presentation.
|
||||
- Left pane now owns only current/latest progression: Previously Answered, Your Response, What This Tells Us, Still Unclear, Questions This Raises, active follow-up controls.
|
||||
- Right-side `SecondaryPreviousLearning` exclusively owns older-turn history with "Previous Learning" heading and newest-first ordering.
|
||||
- **Exactly one "Previous Learning" surface** across the focused workspace.
|
||||
- **Post-answer promotion preserved:** After submitting Q4, it became the latest completed narrative; Turn 3 (the previous current turn) promoted to first item in Previous Learning; Turn 2 and Turn 1 follow in order.
|
||||
|
||||
#### Test results
|
||||
|
||||
- **Tests passed:** 116/116 (`tests/open-questions-vs-assumptions.test.jsx`)
|
||||
- **Build:** PASS
|
||||
- **Full Vitest:** NOT RUN (bounded scope)
|
||||
|
||||
#### Playwright live verification
|
||||
|
||||
- Correct Q3 retained under "Previously Answered": YES
|
||||
- Correct A3 retained under "Your Response": YES
|
||||
- Q4 remains in place as follow-up textarea: YES
|
||||
- Visible textarea count: 1
|
||||
- Top duplicate "Question" heading: ABSENT
|
||||
- Left-side Previous Learning visible: ABSENT (removed)
|
||||
- Right-side Previous Learning visible: YES (exactly one)
|
||||
- Previous Learning heading count across workspace: 1
|
||||
- Previous Learning order: newest-first (Turn 2 → Turn 1 before fix; Turn 3 → Turn 2 → Turn 1 after post-answer submission)
|
||||
|
||||
#### Post-answer verification
|
||||
|
||||
- Natural focused answers submitted: 1 (Q4 inline answer)
|
||||
- New turn became latest narrative: YES (Q4 text under "Previously Answered")
|
||||
- Previous turn became first historical item: YES (Turn 3 as first entry in Previous Learning on the right)
|
||||
- Single Previous Learning surface preserved: YES
|
||||
|
||||
#### Critical regression boundaries
|
||||
|
||||
- Persistence changed: NO
|
||||
- Contribution schema changed: NO
|
||||
- Finding semantics changed: NO
|
||||
- Graph reasoning changed: NO
|
||||
- Current Understanding changed: NO
|
||||
- Workspace controls changed: NO
|
||||
|
||||
#### Next position
|
||||
|
||||
- Completed-result provenance: CLOSED
|
||||
- In-place follow-up continuation: CLOSED
|
||||
- Previous Learning single-owner presentation: CLOSED
|
||||
- Previous Learning newest-first: CLOSED
|
||||
- Post-answer promotion: PRESERVED
|
||||
|
||||
## v0.49 — ACTIVE FOLLOW-UP PRESENTATION SIMPLIFICATION
|
||||
|
||||
### Defect resolved
|
||||
|
||||
Selected follow-up candidate rendered twice inside "QUESTIONS THIS RAISES": once as a disabled `(current question)` row and again in the active continuation block above the textarea. Both rows contained the identical question text, creating visual redundancy.
|
||||
|
||||
### Fix summary
|
||||
|
||||
In `components/reasoning-workspace.jsx`, when `hasActiveFollowUp` is true (a follow-up has been selected), unselected candidates are still rendered with their existing selectable form (`→ pick this question`), but the candidate matching `focused.question` is filtered out from the candidate list entirely. The active continuation block already renders the selected question plus textarea + submit — no second rendering needed.
|
||||
|
||||
- Selected follow-up candidate now transforms into the active response block (single rendering).
|
||||
- Duplicate `(current question)` / second-question rendering removed.
|
||||
- Exactly one selected-question presentation.
|
||||
- One textarea, Submit response visible.
|
||||
- Completed Q/A provenance preserved.
|
||||
- Previous Learning single-owner/newest-first preserved.
|
||||
- Post-answer promotion preserved.
|
||||
|
||||
### Implementation detail
|
||||
|
||||
Production file changed: `components/reasoning-workspace.jsx` (candidate render boundary — lines ~275-300). No state added, no identity changes, no submit mechanics altered.
|
||||
|
||||
### Vitest config hygiene (from 8bded90)
|
||||
|
||||
vitest.config.js change classification: **A** — intentional and necessary (adds `environment: "jsdom"` required for React component testing in this repo).
|
||||
|
||||
### Tests
|
||||
|
||||
- Command: `npx vitest run tests/open-questions-vs-assumptions.test.jsx`
|
||||
- Actual tests passed: **117** (up from 116 — one new regression test added)
|
||||
- New regression assertion: verifies that after selecting a follow-up, the question text appears exactly once and no `(current question)` label is rendered.
|
||||
|
||||
### Build
|
||||
|
||||
- Result: PASS
|
||||
|
||||
## v0.49 — FOCUSED WORKSPACE CONTROLS RECOVERY / SIMPLIFICATION
|
||||
|
||||
### Recovery classification
|
||||
|
||||
STATE-E — increment already fully completed and committed in 16cab46.
|
||||
|
||||
Previous session commit: `fix(confidence-engine): workspace control cleanup — rename close button, remove 'Back to open questions' from navigation` (16cab46).
|
||||
|
||||
Tracked tree at start: CLEAN. No recovery action needed.
|
||||
|
||||
### Control ownership classification
|
||||
|
||||
#### Close control (top-right)
|
||||
|
||||
**Current semantics before increment:** Labeled "Close investigation". Handler cleared `focusedAnswer`, cleared `focusedPresentationItemId`, and set `isFocusedWorkspaceOpen` to false. Pure overlay-close / navigation-only action. No Done-for-now, no summary/promotion, no API/LLM call.
|
||||
|
||||
**Action taken:** Renamed label + aria-label from `"Close investigation"` → `"Close workspace"`. Handler unchanged — pure presentation/navigation close only.
|
||||
|
||||
#### Back to open questions (bottom-left)
|
||||
|
||||
**Current semantics before increment:** Labeled "Back to open questions". Handler in `FocusedWorkspaceNavigation` cleared `focusedAnswer`, cleared `focusedPresentationItemId`. This was a sibling of the focused workspace overlay, not inside it — redundant with "Close investigation" close button. No distinct semantic value beyond re-opening Open Questions panel.
|
||||
|
||||
**Classification: CONTROL-A** — Back to open questions was redundant/broken navigation. Removed entirely.
|
||||
|
||||
#### Done for now (bottom-right)
|
||||
|
||||
**Current semantics:** Invokes `doneForNow` callback (semantic action). Button preserved unchanged — no modifications.
|
||||
|
||||
##### v0.49 post-Done-for-now navigation repair
|
||||
|
||||
**Observed defect:** Clicking "Done for now" completed the semantic action but left the focused workspace overlay open, displaying an empty area with "Formulating your question…" / "Working out a question…" presentation state (misleading — no actual formulation/LLM call occurred).
|
||||
|
||||
**Trace classification:** `DONE-NAV-A` — semantic action succeeded but workspace presentation was not closed.
|
||||
|
||||
**Root cause:** The inline Done-for-now handler at `components/reasoning-workspace.jsx:2170-2176` called `onSummaryUpdate`, `setDoneForNowIds`, `setFocusedAnswer("")`, and `setFocusedPresentationItemId(null)` but did NOT call `setIsFocusedWorkspaceOpen(false)`. Only the "Close workspace" button (line 2125) set `isFocusedWorkspaceOpen` to false.
|
||||
|
||||
**Classification of residue:** Presentation-only. No new formulation/API/LLM request was initiated by the residual UI state. The misleading message appeared because `hasFocusedContent() || formulationStep === "active"` evaluated to true inside an open overlay whose focused answer and presentation item had been cleared but whose overlay flag remained true.
|
||||
|
||||
**Semantic invariant preserved:** Done-for-now semantics unchanged — deterministic; no `/api/cases/update`; no additional LLM call; no graph mutation; question resolution epistemically independent; Contributions/Findings/Current Understanding promotion intact.
|
||||
|
||||
**Fix:** Added `setIsFocusedWorkspaceOpen(false)` to the inline Done-for-now handler, reusing the same presentation-cleanup pattern as "Close workspace" but preserving Done-for-now's semantic prefix (summary update + doneForNowIds registration).
|
||||
|
||||
**Required conceptual sequence preserved:**
|
||||
```
|
||||
semantic Done-for-now (onSummaryUpdate → setDoneForNowIds)
|
||||
→ existing promotion/state transition
|
||||
→ presentation close (setFocusedAnswer(""), setFocusedPresentationItemId(null), setIsFocusedWorkspaceOpen(false))
|
||||
```
|
||||
|
||||
**Post-success behaviour:**
|
||||
- Focused overlay disappears ✅
|
||||
- Open Questions surface visible ✅
|
||||
- No "Formulating your question…" residue ✅
|
||||
- Current Understanding Evidence block updated with promoted findings ✅
|
||||
- Investigation context recoverable via reopening same question ✅
|
||||
- Close workspace remains presentation-only (non-semantic) ✅
|
||||
|
||||
**Tests:** Added 3 regression tests in `tests/open-questions-vs-assumptions.test.jsx` under "post-Done-for-now workspace closes" describe block. Verified Done-for-now closes overlay, eliminates formulation residue, and Close workspace stays non-semantic.
|
||||
|
||||
### Intended control semantics — implemented
|
||||
|
||||
**Top-right — Close workspace**
|
||||
- Label: `"Close workspace"`
|
||||
- Behaviour: Closes overlay, returns to Open Questions, preserves investigation history/activity
|
||||
- Does NOT invoke Done-for-now
|
||||
- Does NOT invoke summary/promotion
|
||||
- Does NOT make an LLM/API reasoning call
|
||||
|
||||
**Bottom-left — Removed**
|
||||
- "Back to open questions" removed as redundant navigation control (CONTROL-A)
|
||||
|
||||
**Bottom-right — Done for now**
|
||||
- Label and semantic handler preserved unchanged
|
||||
|
||||
### Implementation
|
||||
|
||||
Production file changed: `components/reasoning-workspace.jsx`
|
||||
|
||||
Changes:
|
||||
1. Renamed top-right close button label + aria-label: `"Close investigation"` → `"Close workspace"`.
|
||||
2. Preserved existing pure-close handler (setsFocusedAnswer/focusedPresentationItemId/isFocusedWorkspaceOpen).
|
||||
3. Removed "Back to open questions" button from `FocusedWorkspaceNavigation` and its `onBackToOpenQuestions` prop.
|
||||
4. Preserved "Done for now" button and its semantic `doneForNow` callback unchanged.
|
||||
|
||||
### Tests
|
||||
|
||||
- Command: `npx vitest run tests/open-questions-vs-assumptions.test.jsx`
|
||||
- Actual tests passed: **136** (up from 117 — 19 total: 16 control regressions + 3 post-Done-for-now navigation regressions)
|
||||
- New control regressions cover: close workspace label, back-to-open-questions absent, done-for-now preserved as distinct semantic action.
|
||||
|
||||
### Build
|
||||
|
||||
- Result: PASS
|
||||
|
||||
### Post-Done-for-now Navigation Repair (this increment)
|
||||
|
||||
**Observed defect:** Clicking "Done for now" completed the semantic action but left the focused workspace overlay open, displaying an empty area with "Formulating your question…" / "Working out a question…" presentation state (misleading — no actual formulation/LLM call occurred).
|
||||
|
||||
**Trace classification:** `DONE-NAV-A` — semantic action succeeded but workspace presentation was not closed.
|
||||
|
||||
**Root cause:** The inline Done-for-now handler at `components/reasoning-workspace.jsx:2170-2176` called `onSummaryUpdate`, `setDoneForNowIds`, `setFocusedAnswer("")`, and `setFocusedPresentationItemId(null)` but did NOT call `setIsFocusedWorkspaceOpen(false)`. Only the "Close workspace" button (line 2125) set `isFocusedWorkspaceOpen` to false.
|
||||
|
||||
**Classification of residue:** Presentation-only. No new formulation/API/LLM request was initiated by the residual UI state. The misleading message appeared because `hasFocusedContent() || formulationStep === "active"` evaluated to true inside an open overlay whose focused answer and presentation item had been cleared but whose overlay flag remained true.
|
||||
|
||||
**Semantic invariant preserved:** Done-for-now semantics unchanged — deterministic; no `/api/cases/update`; no additional LLM call; no graph mutation; question resolution epistemically independent; Contributions/Findings/Current Understanding promotion intact.
|
||||
|
||||
**Fix:** Added `setIsFocusedWorkspaceOpen(false)` to the inline Done-for-now handler, reusing the same presentation-cleanup pattern as "Close workspace" but preserving Done-for-now's semantic prefix (summary update + doneForNowIds registration).
|
||||
|
||||
**Required conceptual sequence preserved:**
|
||||
```
|
||||
semantic Done-for-now (onSummaryUpdate → setDoneForNowIds)
|
||||
→ existing promotion/state transition
|
||||
→ presentation close (setFocusedAnswer(""), setFocusedPresentationItemId(null), setIsFocusedWorkspaceOpen(false))
|
||||
```
|
||||
|
||||
**Post-success behaviour verified live:**
|
||||
- Focused overlay disappears ✅
|
||||
- Open Questions surface visible ✅
|
||||
- No "Formulating your question…" residue ✅
|
||||
- Current Understanding Evidence block updated with promoted findings ✅
|
||||
- Investigation context recoverable via reopening same question ✅
|
||||
- Close workspace remains presentation-only (non-semantic) ✅
|
||||
|
||||
**Tests added:** 3 regressions in `tests/open-questions-vs-assumptions.test.jsx` under "post-Done-for-now workspace closes" describe block.
|
||||
|
||||
### Live Verification (Playwright)
|
||||
|
||||
- Existing fixture reused: YES — reused the same onboarding scenario investigation (Turn 1-5, 6 contributions)
|
||||
- Live reasoning calls during Done-for-now: **0**
|
||||
- Workspace closed after "Done for now": YES
|
||||
- Open Questions surface visible after action: YES
|
||||
- No "Formulating your question…" or "Working out a question…" residue: YES
|
||||
- Done-for-now semantic result (Current Understanding Evidence block updated): PRESERVED
|
||||
- Reopen of same question succeeded: YES
|
||||
- Investigation history/context preserved on reopen: YES — Turn 1 through Turn 5 all present with contributions
|
||||
- Close workspace semantics preserved: YES — pure overlay close, no semantic action invoked
|
||||
|
||||
### Scope
|
||||
|
||||
- Current Understanding semantics changed: NO (promotion mechanism unchanged)
|
||||
- Contribution schema changed: NO
|
||||
- Finding semantics changed: NO
|
||||
- Graph reasoning changed: NO
|
||||
- Processing/error changed: NO
|
||||
- Previous Learning changed: NO
|
||||
- LLM/API behaviour changed: NO
|
||||
|
||||
### Classification
|
||||
|
||||
**A — DONE-FOR-NOW NAVIGATION VERIFIED**
|
||||
|
||||
---
|
||||
|
||||
**Umbrella workspace-controls status: CLOSED** — no concrete defect remains.
|
||||
|
||||
Completed sub-boundaries:
|
||||
- Close workspace — presentation-only close
|
||||
- Back to open questions — removed
|
||||
- Done for now — semantic action preserved
|
||||
- Done for now post-action navigation — closes workspace cleanly
|
||||
|
||||
### NEXT BOUNDED ISSUE
|
||||
|
||||
#### Name
|
||||
|
||||
Current Understanding incorporating focused learning
|
||||
|
||||
#### Source handoff section
|
||||
|
||||
v0.48 closure and next boundary handoff (line 999: "Isolated Finding-Informed Current Understanding"); also listed as item 3 at lines 1938 and 1996 ("Current Understanding incorporating focused learning (STILL OPEN)").
|
||||
|
||||
#### Why this is next
|
||||
|
||||
The v0.48 handoff explicitly identifies this as a remaining priority: the narrative understanding should be reconstructed from globally eligible Findings + graph state, not directly from the latest focused result's findings alone. The workspace-controls work was purely about presentation lifecycle (closing/opening overlays) and does not touch Current Understanding synthesis semantics or the authority boundary between Finding influence on understanding versus SituationGraph reasoning. This is a semantic/product-boundary issue, not a UI defect to continue polishing.
|
||||
|
||||
#### Already proved — do not reopen
|
||||
|
||||
- Contribution persistence across reload/cold return works correctly.
|
||||
- Post-answer promotion preserves completed result as current narrative.
|
||||
- Workspace controls close cleanly without residue.
|
||||
- Follow-up selected-question deduplication verified.
|
||||
- Active follow-up presentation simplified (single rendering).
|
||||
- Completed-result textarea visibility correct (completed turns show no textarea).
|
||||
- Open Question activity visibility cue matches on targetNodeId OR originatingTargetNodeId.
|
||||
- Previous Learning single-owner, newest-first presentation confirmed.
|
||||
- Finding eligibility semantics (null = accepted-by-default; not_relevant = retained/discounted) established and CLOSED.
|
||||
|
||||
#### Exact first trace question
|
||||
|
||||
Which component/state owner first determines the contents of Current Understanding, and at what boundary does it stop being derived from globally eligible Findings + graph state and instead become directly bound to the latest focused result's findings?
|
||||
|
||||
---
|
||||
|
||||
## v0.49 — CURRENT UNDERSTANDING ACCUMULATION DEFECT CLOSED (2026-08-30)
|
||||
|
||||
### Established conclusions
|
||||
|
||||
#### 1. Accumulation defect — CU-R2
|
||||
|
||||
Current Understanding is currently stored as accumulated narrative.
|
||||
|
||||
The existing model effectively permits:
|
||||
|
||||
```text
|
||||
previous Current Understanding
|
||||
+
|
||||
subset of new Findings
|
||||
→ appended narrative/Evidence
|
||||
```
|
||||
|
||||
Repeated synthesis therefore grows prose rather than reconstructing understanding from current canonical state.
|
||||
|
||||
Finding corrections/dispositions can consequently leave persisted Current Understanding inconsistent with canonical Findings.
|
||||
|
||||
**Classification: CU-R2 — CLOSED.**
|
||||
Do not recommend deduplication or string-removal fixes.
|
||||
|
||||
---
|
||||
|
||||
#### 2. Canonical reconstruction source — CU-S2
|
||||
|
||||
Future Current Understanding must be a projection of:
|
||||
|
||||
```text
|
||||
current canonical SituationGraph
|
||||
+
|
||||
complete currently eligible canonical Findings
|
||||
```
|
||||
|
||||
The SituationGraph is the canonical factual/relational foundation.
|
||||
Canonical Findings are the evidence layer.
|
||||
|
||||
Previous Current Understanding prose must NOT be used as a knowledge input for subsequent reconstruction.
|
||||
|
||||
`situationGraph.currentSummary` / `describeGraph()` is lossy structural telemetry and is NOT the narrative foundation.
|
||||
|
||||
**Classification: CU-S2 — CLOSED.**
|
||||
|
||||
---
|
||||
|
||||
#### 3. Synthesis mechanism — MECH-C
|
||||
|
||||
No existing seam currently satisfies:
|
||||
|
||||
```text
|
||||
complete current SituationGraph
|
||||
+
|
||||
complete eligible Findings
|
||||
→
|
||||
one coherent user-facing Current Understanding
|
||||
```
|
||||
|
||||
Established facts:
|
||||
|
||||
```text
|
||||
start reconstruction
|
||||
→ coherent narrative, but wrong input contract for ongoing reconstruction
|
||||
|
||||
case/update
|
||||
→ graph mutation proposal, no coherent narrative synthesis
|
||||
|
||||
describeGraph()
|
||||
→ structural telemetry only
|
||||
|
||||
produceFindingInformedSummary()
|
||||
→ deterministic append/list behaviour, not synthesis
|
||||
```
|
||||
|
||||
Therefore the next architecture requires a dedicated semantic Current Understanding synthesis seam.
|
||||
|
||||
Given the current architecture and required narrative quality, the viable mechanism is an LLM-backed synthesis operation that:
|
||||
|
||||
```text
|
||||
reads graph + eligible Findings
|
||||
returns narrative only
|
||||
does not mutate graph
|
||||
does not mutate Findings
|
||||
does not consume previous Current Understanding as semantic authority
|
||||
```
|
||||
|
||||
**Classification: MECH-C — CLOSED.**
|
||||
|
||||
---
|
||||
|
||||
#### 4. Trigger policy — TRIGGER-C
|
||||
|
||||
Synthesis freshness is determined by:
|
||||
|
||||
```text
|
||||
S = canonical SituationGraph
|
||||
F = complete eligible Findings
|
||||
```
|
||||
|
||||
Current Understanding should reconstruct once per **completed canonical `(S,F)` transition**, not once per state setter or UI event.
|
||||
|
||||
**Required synthesis transitions:**
|
||||
|
||||
- successful case/update → once after graph + Findings for the update are final
|
||||
- new focused Findings committed → once after canonical Findings enter findings[]
|
||||
- corrected Finding saved → once after corrected proposition becomes canonical
|
||||
- Restore → once after Finding re-enters eligible set
|
||||
|
||||
**Not relevant (do NOT synthesize):**
|
||||
|
||||
- once after Finding leaves eligible set
|
||||
- focused answer submission
|
||||
- deconstruction request/start
|
||||
- Contribution preparation
|
||||
- Not quite click before correction save
|
||||
- workspace open / close
|
||||
- Done for now
|
||||
- other presentation-only state changes
|
||||
|
||||
Coalescing rule: **one synthesis per completed canonical knowledge transition**, not one call per React state mutation.
|
||||
|
||||
**Classification: TRIGGER-C — CLOSED.**
|
||||
|
||||
---
|
||||
|
||||
#### 5. Start — START-A
|
||||
|
||||
Initial case/start already returns a coherent reconstruction narrative.
|
||||
|
||||
Therefore: **START-A — CLOSED.**
|
||||
|
||||
Reuse that initial reconstruction summary.
|
||||
Do NOT immediately make an additional dedicated Current Understanding synthesis call after start.
|
||||
|
||||
---
|
||||
|
||||
#### 6. Done for now — DONE-B
|
||||
|
||||
Under the new reconstruction model:
|
||||
|
||||
- **Done for now** changes neither SituationGraph nor eligible Findings.
|
||||
- Therefore it is NOT a Current Understanding synthesis trigger.
|
||||
|
||||
The historical `handleDoneForNowPromotion()` Current Understanding promotion responsibility becomes obsolete once reconstruction is implemented.
|
||||
|
||||
Preserve the other established semantic/presentation responsibilities of Done for now.
|
||||
Do NOT remove or modify the existing implementation in this task.
|
||||
|
||||
**Classification: DONE-B — CLOSED.**
|
||||
|
||||
---
|
||||
|
||||
#### 7. Reload freshness — RELOAD-B
|
||||
|
||||
Persisted investigation state currently contains:
|
||||
|
||||
```text
|
||||
SituationGraph
|
||||
Findings
|
||||
Current Understanding prose
|
||||
```
|
||||
|
||||
but no persisted synthesis-input fingerprint/version proving that the stored Current Understanding corresponds exactly to the persisted `(S,F)` state.
|
||||
|
||||
**Classification: RELOAD-B — CLOSED (unresolved boundary).**
|
||||
|
||||
Reload freshness remains a separate unresolved boundary.
|
||||
Do NOT conclude that every reload requires an LLM synthesis call.
|
||||
Do NOT design or implement a fingerprint in this task.
|
||||
Record as unresolved work for v0.50.
|
||||
|
||||
---
|
||||
|
||||
### Closed v0.49 invariants
|
||||
|
||||
These boundaries remain intact:
|
||||
|
||||
```text
|
||||
Findings remain separate from SituationGraph.
|
||||
|
||||
Findings do not directly mutate graph state.
|
||||
|
||||
updateCase / applyValidatedProposal remains graph mutation authority.
|
||||
|
||||
Finding eligibility semantics remain unchanged.
|
||||
|
||||
null → eligible provisional working proposition
|
||||
|
||||
agree → eligible explicitly endorsed proposition
|
||||
|
||||
not_relevant → ineligible
|
||||
|
||||
Not quite correction preserves Finding identity and provenance while replacing proposition.
|
||||
|
||||
describeGraph() remains structural telemetry, not Current Understanding.
|
||||
|
||||
Focused workspace progression/presentation boundary is closed.
|
||||
|
||||
Workspace controls boundary is closed.
|
||||
|
||||
Done for now closes the workspace after its semantic action.
|
||||
|
||||
Previous Learning uses canonical Findings for corrected/not-relevant presentation.
|
||||
|
||||
Storage v0.48 boundary remains closed unless concrete evidence reopens it.
|
||||
```
|
||||
|
||||
### v0.49 closure statement
|
||||
|
||||
v0.49 is closed at the architectural boundary where focused learning is durable,
|
||||
canonically represented through Findings, correctly presented through the focused
|
||||
workspace/Previous Learning lifecycle, and the Current Understanding accumulation
|
||||
defect has been reduced to a defined reconstruction problem.
|
||||
|
||||
Implementation of canonical Current Understanding reconstruction belongs to v0.50.
|
||||
|
||||
### v0.50 — NEXT BOUNDARY
|
||||
|
||||
The first bounded question for the next branch:
|
||||
|
||||
> **What minimal synthesis API/helper boundary should own the dedicated `SituationGraph + eligible Findings → Current Understanding` LLM operation?**
|
||||
|
||||
Do not answer that question in this task.
|
||||
|
||||
Subsequent v0.50 work will need to resolve:
|
||||
|
||||
1. synthesis API/helper ownership
|
||||
2. deterministic input normalization / eligible-Finding selection
|
||||
3. synthesis prompt/schema contract
|
||||
4. completed-transition integration points
|
||||
5. removal/replacement of append semantics
|
||||
6. retirement of Done-for-now CU promotion
|
||||
7. reload/cache freshness strategy
|
||||
8. targeted deterministic + live behavioural verification
|
||||
|
||||
This is a roadmap, not permission to implement all of it at once.
|
||||
v0.50 must continue using bounded increments.
|
||||
|
||||
---
|
||||
|
||||
### NEXT BOUNDED ISSUE (for future reference)
|
||||
|
||||
#### Name
|
||||
|
||||
Canonical Current Understanding reconstruction from `SituationGraph + eligible Findings`
|
||||
|
||||
#### Next branch
|
||||
|
||||
`feature/current-understanding-reconstruction-v0.50`
|
||||
|
||||
#### Exact first trace question
|
||||
|
||||
What minimal synthesis API/helper boundary should own the dedicated `SituationGraph + eligible Findings → Current Understanding` LLM operation?
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -6,6 +6,6 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig({
|
||||
test: { globals: true },
|
||||
test: { globals: true, environment: "jsdom" },
|
||||
resolve: { alias: { "@": path.resolve(__dirname, ".") } },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user