Merge pull request #733 from AssetOverflow/feat/wb-c2b-contemplation-process
Wave M C2-b: contemplation as a legible staged process
This commit is contained in:
commit
dedaa0b7dd
8 changed files with 306 additions and 29 deletions
|
|
@ -160,6 +160,20 @@ Detailed brief pack: `docs/handoff/wave-M-phaseB-calibration-briefs-2026-06-13.m
|
||||||
ratification boundary, and grounded-after scenes remain report-authored
|
ratification boundary, and grounded-after scenes remain report-authored
|
||||||
evidence. This is the first process trace; the fuller Calibration/Proposal
|
evidence. This is the first process trace; the fuller Calibration/Proposal
|
||||||
integrated loop is still open.
|
integrated loop is still open.
|
||||||
|
**C2-b implementation note (2026-06-13):** the run detail is no longer a flat
|
||||||
|
JSON dump — each scene is now a typed loop stage. The reader
|
||||||
|
(`_contemplation_scenes`) projects every scene onto a canonical
|
||||||
|
`stage_role` (`cold_attempt`/`engine_enrichment`/`engine_proposal`/
|
||||||
|
`operator_ratifies`/`grounded`/`other`) and pulls the loop's connective ids
|
||||||
|
(`proposal_id`, `candidate_id`, `proposal_state`, `grounding_source`) out of
|
||||||
|
the raw detail; the UI renders the arc "attempt → enrich → propose → ratify →
|
||||||
|
grounded" with named stages, cold→grounded bookends, and the ids as evidence.
|
||||||
|
**Honest wrinkle (surfaced, not faked):** the fixture proposals do NOT resolve
|
||||||
|
in the live proposal log (source_kinds `exemplar_corpus`/`operator`, none
|
||||||
|
`contemplation`), so the proposal id is shown as evidence but is intentionally
|
||||||
|
NOT a clickable cross-route link — a dead link would be theater. Live
|
||||||
|
Proposals/Calibration navigation is deferred until real contemplation
|
||||||
|
proposals reach the log (reader-verified linking is the follow-up).
|
||||||
- **C3 — Field substrate (honest, read-only, hard):** real `FieldState` +
|
- **C3 — Field substrate (honest, read-only, hard):** real `FieldState` +
|
||||||
`versor_condition` for a turn, rendered as **inspectable exact numbers and
|
`versor_condition` for a turn, rendered as **inspectable exact numbers and
|
||||||
invariant status** — `versor_condition < 1e-6` as a live "field is valid"
|
invariant status** — `versor_condition < 1e-6` as a live "field is valid"
|
||||||
|
|
|
||||||
76
tests/test_workbench_contemplation_reader.py
Normal file
76
tests/test_workbench_contemplation_reader.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""C2-b — the contemplation loop as a typed staged projection.
|
||||||
|
|
||||||
|
The reader turns each scene into a canonical loop stage (cold attempt → engine
|
||||||
|
enrichment → engine-authored proposal → operator ratifies → grounded) and pulls
|
||||||
|
the connective ids (proposal/candidate/state/grounding) out of the raw detail so
|
||||||
|
the UI renders a process, not a JSON dump.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from workbench import readers
|
||||||
|
|
||||||
|
|
||||||
|
class TestStageRole:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scene_id,role",
|
||||||
|
[
|
||||||
|
("S1_cold_session", "cold_attempt"),
|
||||||
|
("S2_checkpoint_enrichment", "engine_enrichment"),
|
||||||
|
("S3_engine_authored_proposal", "engine_proposal"),
|
||||||
|
("S4_operator_ratifies", "operator_ratifies"),
|
||||||
|
("S5_grounded_session", "grounded"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_canonical_arc_ids_map_to_roles(self, scene_id: str, role: str) -> None:
|
||||||
|
assert readers._contemplation_stage_role(scene_id) == role
|
||||||
|
|
||||||
|
def test_unknown_scene_id_falls_back_to_other(self) -> None:
|
||||||
|
# Honest closed set: anything outside the arc is "other", never guessed.
|
||||||
|
assert readers._contemplation_stage_role("S9_unmapped_thing") == "other"
|
||||||
|
assert readers._contemplation_stage_role("") == "other"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSceneProjection:
|
||||||
|
def test_connective_ids_are_pulled_from_detail(self) -> None:
|
||||||
|
report = {
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"scene": "S1_cold_session",
|
||||||
|
"claim": "cold",
|
||||||
|
"detail": {"grounding_source": "none"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene": "S2_checkpoint_enrichment",
|
||||||
|
"claim": "enrich",
|
||||||
|
"detail": {"candidate_id": "cand-123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scene": "S3_engine_authored_proposal",
|
||||||
|
"claim": "propose",
|
||||||
|
"detail": {"proposal_id": "prop-456", "state": "pending"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
scenes = readers._contemplation_scenes(report)
|
||||||
|
s1, s2, s3 = scenes
|
||||||
|
assert (s1.stage_role, s1.grounding_source) == ("cold_attempt", "none")
|
||||||
|
assert (s2.stage_role, s2.candidate_id) == ("engine_enrichment", "cand-123")
|
||||||
|
assert s3.stage_role == "engine_proposal"
|
||||||
|
assert s3.proposal_id == "prop-456"
|
||||||
|
assert s3.proposal_state == "pending"
|
||||||
|
# Absent ids stay None, never empty strings.
|
||||||
|
assert s1.proposal_id is None and s1.candidate_id is None
|
||||||
|
|
||||||
|
def test_detail_is_preserved_for_the_inspector(self) -> None:
|
||||||
|
report = {"scenes": [{"scene": "S1_cold_session", "claim": "c", "detail": {"x": 1}}]}
|
||||||
|
(scene,) = readers._contemplation_scenes(report)
|
||||||
|
assert scene.detail == {"x": 1}
|
||||||
|
|
||||||
|
def test_non_dict_scene_is_skipped(self) -> None:
|
||||||
|
report = {"scenes": ["not a scene", {"scene": "S1_cold_session", "claim": "c"}]}
|
||||||
|
scenes = readers._contemplation_scenes(report)
|
||||||
|
assert len(scenes) == 1
|
||||||
|
assert scenes[0].stage_role == "cold_attempt"
|
||||||
|
|
@ -103,7 +103,12 @@
|
||||||
"ContemplationScene": [
|
"ContemplationScene": [
|
||||||
"scene_id",
|
"scene_id",
|
||||||
"claim",
|
"claim",
|
||||||
"detail"
|
"detail",
|
||||||
|
"stage_role",
|
||||||
|
"proposal_id",
|
||||||
|
"candidate_id",
|
||||||
|
"proposal_state",
|
||||||
|
"grounding_source"
|
||||||
],
|
],
|
||||||
"DemoDagEdge": [
|
"DemoDagEdge": [
|
||||||
"from_node",
|
"from_node",
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,21 @@ const detail: ContemplationRunDetail = {
|
||||||
scene_id: "S1_cold_session",
|
scene_id: "S1_cold_session",
|
||||||
claim: "cold session refused",
|
claim: "cold session refused",
|
||||||
detail: { grounding_source: "none" },
|
detail: { grounding_source: "none" },
|
||||||
|
stage_role: "cold_attempt",
|
||||||
|
proposal_id: null,
|
||||||
|
candidate_id: null,
|
||||||
|
proposal_state: null,
|
||||||
|
grounding_source: "none",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scene_id: "S3_engine_authored_proposal",
|
scene_id: "S3_engine_authored_proposal",
|
||||||
claim: "proposal remains pending",
|
claim: "proposal remains pending",
|
||||||
detail: { state: "pending" },
|
detail: { state: "pending", proposal_id: "05e167163c1f22c1" },
|
||||||
|
stage_role: "engine_proposal",
|
||||||
|
proposal_id: "05e167163c1f22c1",
|
||||||
|
candidate_id: null,
|
||||||
|
proposal_state: "pending",
|
||||||
|
grounding_source: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -122,6 +132,28 @@ describe("ContemplationRoute", () => {
|
||||||
expect(screen.getByText(/\"connective\":\"reveals\"/)).toBeInTheDocument();
|
expect(screen.getByText(/\"connective\":\"reveals\"/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders the loop as a staged process with connective evidence, no dead link", async () => {
|
||||||
|
stubFetch();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderRoute();
|
||||||
|
|
||||||
|
await user.click(await screen.findByText("Why does narrative exist?"));
|
||||||
|
await screen.findByText("Process Trace");
|
||||||
|
|
||||||
|
// Canonical stage roles are named, not raw scene ids alone.
|
||||||
|
expect(screen.getByText("Cold attempt")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Engine-authored proposal")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/attempt → enrich → propose → ratify → grounded/),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// The proposal id is surfaced as evidence with its state...
|
||||||
|
const evidence = screen.getByText(/05e167163c1f22c1 · pending/);
|
||||||
|
expect(evidence).toBeInTheDocument();
|
||||||
|
// ...but NOT as a clickable link (it does not resolve in the live log).
|
||||||
|
expect(evidence.closest("a")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders the honest absence state when no reports exist", async () => {
|
it("renders the honest absence state when no reports exist", async () => {
|
||||||
stubFetch([]);
|
stubFetch([]);
|
||||||
renderRoute();
|
renderRoute();
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,21 @@ import type {
|
||||||
ContemplationRunDetail,
|
ContemplationRunDetail,
|
||||||
ContemplationRunSummary,
|
ContemplationRunSummary,
|
||||||
ContemplationScene,
|
ContemplationScene,
|
||||||
|
ContemplationStageRole,
|
||||||
} from "../../types/api";
|
} from "../../types/api";
|
||||||
import { pushRecentItem } from "../commandRegistry";
|
import { pushRecentItem } from "../commandRegistry";
|
||||||
|
|
||||||
|
// The contemplation loop made legible: attempt → enrich → propose → ratify →
|
||||||
|
// grounded. Labels for the canonical ADR-0172 stage roles.
|
||||||
|
const STAGE_LABEL: Record<ContemplationStageRole, string> = {
|
||||||
|
cold_attempt: "Cold attempt",
|
||||||
|
engine_enrichment: "Engine enrichment",
|
||||||
|
engine_proposal: "Engine-authored proposal",
|
||||||
|
operator_ratifies: "Operator ratifies",
|
||||||
|
grounded: "Grounded",
|
||||||
|
other: "Stage",
|
||||||
|
};
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
function errorMessage(error: unknown): string {
|
||||||
return error instanceof WorkbenchApiError
|
return error instanceof WorkbenchApiError
|
||||||
? error.message
|
? error.message
|
||||||
|
|
@ -74,7 +86,38 @@ function RunRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SceneCard({ scene, index }: { scene: ContemplationScene; index: number }) {
|
function StageEvidence({ scene }: { scene: ContemplationScene }) {
|
||||||
|
// The loop's connective tissue, pulled out of the raw detail. Ids are
|
||||||
|
// surfaced as evidence (copyable); cross-route navigation is intentionally
|
||||||
|
// NOT a live link yet — these fixture proposals do not resolve in the live
|
||||||
|
// proposal log, and a dead link would be theater.
|
||||||
|
const rows: { label: string; value: string }[] = [];
|
||||||
|
if (scene.grounding_source) rows.push({ label: "grounding", value: scene.grounding_source });
|
||||||
|
if (scene.candidate_id) rows.push({ label: "candidate", value: scene.candidate_id });
|
||||||
|
if (scene.proposal_id) {
|
||||||
|
rows.push({
|
||||||
|
label: "proposal",
|
||||||
|
value: scene.proposal_state
|
||||||
|
? `${scene.proposal_id} · ${scene.proposal_state}`
|
||||||
|
: scene.proposal_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<dl className="mt-2 grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<div key={row.label} className="contents">
|
||||||
|
<dt className="text-[var(--color-text-secondary)]">{row.label}</dt>
|
||||||
|
<dd className="m-0 truncate font-mono text-[var(--color-text-primary)]" title={row.value}>
|
||||||
|
{row.value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessStageCard({ scene, index }: { scene: ContemplationScene; index: number }) {
|
||||||
return (
|
return (
|
||||||
<li className="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-3">
|
<li className="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-3">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
|
@ -83,20 +126,66 @@ function SceneCard({ scene, index }: { scene: ContemplationScene; index: number
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 border-l border-[var(--color-border-subtle)] pl-3">
|
<div className="min-w-0 border-l border-[var(--color-border-subtle)] pl-3">
|
||||||
<div className="font-mono text-xs text-[var(--color-text-muted)]">
|
<div className="flex flex-wrap items-baseline gap-2">
|
||||||
{scene.scene_id}
|
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||||
|
{STAGE_LABEL[scene.stage_role]}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-xs text-[var(--color-text-muted)]">
|
||||||
|
{scene.scene_id}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="m-0 mt-1 text-sm text-[var(--color-text-primary)]">
|
<p className="m-0 mt-1 text-sm text-[var(--color-text-primary)] [text-wrap:balance]">
|
||||||
{scene.claim}
|
{scene.claim}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 max-h-80 overflow-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] p-2">
|
<StageEvidence scene={scene} />
|
||||||
<StableJsonViewer source={JSON.stringify(scene.detail, null, 2)} />
|
<details className="mt-2">
|
||||||
</div>
|
<summary className="cursor-pointer text-xs text-[var(--color-text-secondary)]">
|
||||||
|
raw detail
|
||||||
|
</summary>
|
||||||
|
<div className="mt-1 max-h-80 overflow-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] p-2">
|
||||||
|
<StableJsonViewer source={JSON.stringify(scene.detail, null, 2)} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Bookend({
|
||||||
|
label,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
payload: Record<string, unknown> | null | undefined;
|
||||||
|
}) {
|
||||||
|
const surface = typeof payload?.surface === "string" ? payload.surface : null;
|
||||||
|
const grounding =
|
||||||
|
typeof payload?.grounding_source === "string" ? payload.grounding_source : null;
|
||||||
|
return (
|
||||||
|
<div className="min-w-0 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] p-2">
|
||||||
|
<div className="mb-1 flex items-baseline justify-between gap-2">
|
||||||
|
<span className="text-xs font-semibold text-[var(--color-text-secondary)]">{label}</span>
|
||||||
|
{grounding ? (
|
||||||
|
<span className="font-mono text-xs text-[var(--color-text-muted)]">{grounding}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{surface ? (
|
||||||
|
<p className="m-0 text-sm text-[var(--color-text-primary)] [text-wrap:balance]">{surface}</p>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-[var(--color-text-secondary)]">not recorded</span>
|
||||||
|
)}
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="cursor-pointer text-xs text-[var(--color-text-secondary)]">
|
||||||
|
raw
|
||||||
|
</summary>
|
||||||
|
<div className="mt-1 overflow-auto">
|
||||||
|
<StableJsonViewer source={JSON.stringify(payload ?? {}, null, 2)} />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DetailPanel({ detail }: { detail: ContemplationRunDetail }) {
|
function DetailPanel({ detail }: { detail: ContemplationRunDetail }) {
|
||||||
const digest = digestPayload(detail.source_digest);
|
const digest = digestPayload(detail.source_digest);
|
||||||
return (
|
return (
|
||||||
|
|
@ -127,24 +216,19 @@ function DetailPanel({ detail }: { detail: ContemplationRunDetail }) {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<div className="min-w-0 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] p-2">
|
<Bookend label="Cold session (before)" payload={detail.before} />
|
||||||
<div className="mb-1 text-xs font-semibold text-[var(--color-text-secondary)]">
|
<Bookend label="Grounded session (after)" payload={detail.after} />
|
||||||
before
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<StableJsonViewer source={JSON.stringify(detail.before ?? {}, null, 2)} />
|
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]">
|
||||||
</div>
|
Learning loop · attempt → enrich → propose → ratify → grounded
|
||||||
<div className="min-w-0 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] p-2">
|
</div>
|
||||||
<div className="mb-1 text-xs font-semibold text-[var(--color-text-secondary)]">
|
<ol className="m-0 grid list-none gap-4 p-0">
|
||||||
after
|
{detail.scenes.map((scene, index) => (
|
||||||
</div>
|
<ProcessStageCard key={`${scene.scene_id}-${index}`} scene={scene} index={index} />
|
||||||
<StableJsonViewer source={JSON.stringify(detail.after ?? {}, null, 2)} />
|
))}
|
||||||
</div>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<ol className="m-0 grid list-none gap-4 p-0">
|
|
||||||
{detail.scenes.map((scene, index) => (
|
|
||||||
<SceneCard key={`${scene.scene_id}-${index}`} scene={scene} index={index} />
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -412,10 +412,23 @@ export interface DemoRunResult {
|
||||||
scenarios: DemoScenarioRunResult[];
|
scenarios: DemoScenarioRunResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ContemplationStageRole =
|
||||||
|
| "cold_attempt"
|
||||||
|
| "engine_enrichment"
|
||||||
|
| "engine_proposal"
|
||||||
|
| "operator_ratifies"
|
||||||
|
| "grounded"
|
||||||
|
| "other";
|
||||||
|
|
||||||
export interface ContemplationScene {
|
export interface ContemplationScene {
|
||||||
scene_id: string;
|
scene_id: string;
|
||||||
claim: string;
|
claim: string;
|
||||||
detail: Record<string, unknown>;
|
detail: Record<string, unknown>;
|
||||||
|
stage_role: ContemplationStageRole;
|
||||||
|
proposal_id: string | null;
|
||||||
|
candidate_id: string | null;
|
||||||
|
proposal_state: string | null;
|
||||||
|
grounding_source: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContemplationRunSummary {
|
export interface ContemplationRunSummary {
|
||||||
|
|
|
||||||
|
|
@ -1015,6 +1015,30 @@ def _contemplation_run_paths() -> list[Path]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# scene-id token -> canonical loop stage role. Ordered: first match wins.
|
||||||
|
_CONTEMPLATION_STAGE_TOKENS: tuple[tuple[str, str], ...] = (
|
||||||
|
("cold", "cold_attempt"),
|
||||||
|
("enrich", "engine_enrichment"),
|
||||||
|
("checkpoint", "engine_enrichment"),
|
||||||
|
("proposal", "engine_proposal"),
|
||||||
|
("authored", "engine_proposal"),
|
||||||
|
("ratif", "operator_ratifies"),
|
||||||
|
("grounded", "grounded"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _contemplation_stage_role(scene_id: str) -> str:
|
||||||
|
low = scene_id.lower()
|
||||||
|
for token, role in _CONTEMPLATION_STAGE_TOKENS:
|
||||||
|
if token in low:
|
||||||
|
return role
|
||||||
|
return "other"
|
||||||
|
|
||||||
|
|
||||||
|
def _opt_str(value: Any) -> str | None:
|
||||||
|
return value if isinstance(value, str) and value else None
|
||||||
|
|
||||||
|
|
||||||
def _contemplation_scenes(report: dict[str, Any]) -> list[ContemplationScene]:
|
def _contemplation_scenes(report: dict[str, Any]) -> list[ContemplationScene]:
|
||||||
scenes = report.get("scenes")
|
scenes = report.get("scenes")
|
||||||
if not isinstance(scenes, list):
|
if not isinstance(scenes, list):
|
||||||
|
|
@ -1023,12 +1047,19 @@ def _contemplation_scenes(report: dict[str, Any]) -> list[ContemplationScene]:
|
||||||
for index, scene in enumerate(scenes):
|
for index, scene in enumerate(scenes):
|
||||||
if not isinstance(scene, dict):
|
if not isinstance(scene, dict):
|
||||||
continue
|
continue
|
||||||
detail = scene.get("detail")
|
raw_detail = scene.get("detail")
|
||||||
|
detail = raw_detail if isinstance(raw_detail, dict) else {}
|
||||||
|
scene_id = str(scene.get("scene") or f"scene_{index + 1}")
|
||||||
out.append(
|
out.append(
|
||||||
ContemplationScene(
|
ContemplationScene(
|
||||||
scene_id=str(scene.get("scene") or f"scene_{index + 1}"),
|
scene_id=scene_id,
|
||||||
claim=str(scene.get("claim") or ""),
|
claim=str(scene.get("claim") or ""),
|
||||||
detail=detail if isinstance(detail, dict) else {},
|
detail=detail,
|
||||||
|
stage_role=_contemplation_stage_role(scene_id), # type: ignore[arg-type]
|
||||||
|
proposal_id=_opt_str(detail.get("proposal_id")),
|
||||||
|
candidate_id=_opt_str(detail.get("candidate_id")),
|
||||||
|
proposal_state=_opt_str(detail.get("state")),
|
||||||
|
grounding_source=_opt_str(detail.get("grounding_source")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -377,11 +377,33 @@ class DemoRunResult:
|
||||||
scenarios: list[DemoScenarioRunResult] = field(default_factory=list)
|
scenarios: list[DemoScenarioRunResult] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
# Canonical ADR-0172 learning-arc stages (cold attempt → engine enrichment →
|
||||||
|
# engine-authored proposal → operator ratifies → grounded). "other" is the
|
||||||
|
# explicit fallback for any scene id outside the closed arc.
|
||||||
|
ContemplationStageRole = Literal[
|
||||||
|
"cold_attempt",
|
||||||
|
"engine_enrichment",
|
||||||
|
"engine_proposal",
|
||||||
|
"operator_ratifies",
|
||||||
|
"grounded",
|
||||||
|
"other",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class ContemplationScene:
|
class ContemplationScene:
|
||||||
scene_id: str
|
scene_id: str
|
||||||
claim: str
|
claim: str
|
||||||
detail: dict[str, Any] = field(default_factory=dict)
|
detail: dict[str, Any] = field(default_factory=dict)
|
||||||
|
# Typed projection of the contemplation *loop* — the role this scene plays
|
||||||
|
# and the connective ids that thread it to the proposal / candidate
|
||||||
|
# surfaces. ``proposal_id`` etc. are surfaced as evidence; cross-route
|
||||||
|
# navigation lights up only when the id resolves in the live log.
|
||||||
|
stage_role: ContemplationStageRole = "other"
|
||||||
|
proposal_id: str | None = None
|
||||||
|
candidate_id: str | None = None
|
||||||
|
proposal_state: str | None = None
|
||||||
|
grounding_source: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue