chore: integrate Subject Memory information improvements
This commit is contained in:
@@ -502,7 +502,9 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
|
||||
subjectIndexLoad.includes('"/subjektiv/subjects"') &&
|
||||
subjectIndexLoad.includes("parseSubjektivSubjectListResponse") &&
|
||||
subjectIndexPage.includes(">Subjects</h1>") &&
|
||||
subjectIndexPage.includes("subject.store_revision") &&
|
||||
subjectIndexPage.includes("Worker connection") &&
|
||||
subjectIndexPage.includes("subject.current_worker") &&
|
||||
!subjectIndexPage.includes("subject.store_revision") &&
|
||||
subjectIndexPage.includes('data-memory-view="subjects"'),
|
||||
"Memory product entry should list explicit typed subjects",
|
||||
);
|
||||
@@ -510,10 +512,13 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
|
||||
subjectPageLoad.includes("`${subjectPath}/surface`") &&
|
||||
subjectPageLoad.includes("`${subjectPath}/memories`") &&
|
||||
subjectPage.includes("DocumentMarkdown from") &&
|
||||
subjectPage.includes("Resident surface") &&
|
||||
subjectPage.includes("Resident context") &&
|
||||
subjectPage.includes("Subject store revision") &&
|
||||
subjectPage.includes("Current Memories") &&
|
||||
subjectPage.includes("surface?.availability === 'ready'") &&
|
||||
subjectPage.includes("surface?.availability === 'ungenerated'") &&
|
||||
subjectPage.includes("surface?.availability === 'stale'") &&
|
||||
subjectPage.includes("surface?.availability === 'failed'") &&
|
||||
subjectPage.includes("data-surface-ready-empty") &&
|
||||
!subjectPage.includes("overflow-y"),
|
||||
"Subject page should combine strict resident-surface and current-Memory reads without a nested vertical scroller",
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
||||
}
|
||||
|
||||
function subjectStateLabel(state: SubjektivSubjectResponse['state']): string {
|
||||
return state === 'active' ? 'Available' : 'Retired';
|
||||
}
|
||||
|
||||
async function openCreateForm(): Promise<void> {
|
||||
createExpanded = true;
|
||||
createError = null;
|
||||
@@ -109,7 +113,7 @@
|
||||
</div>
|
||||
<div class="memory-header-actions">
|
||||
{#if data.subjects.data}
|
||||
<span class="memory-count">{subjects.length}{data.subjects.data.has_more ? '+' : ''} subject{subjects.length === 1 ? '' : 's'}</span>
|
||||
<span class="memory-count">{subjects.length} shown{data.subjects.data.has_more ? ' · more available' : ''}</span>
|
||||
{/if}
|
||||
<button
|
||||
bind:this={createButton}
|
||||
@@ -184,14 +188,19 @@
|
||||
{#each subjects as subject (subject.id)}
|
||||
<a class="subject-row" href={subjectHref(subject.id)}>
|
||||
<div class="subject-copy">
|
||||
<span class="memory-state-pill is-{subject.state}">{subject.state}</span>
|
||||
<span class="memory-state-pill is-{subject.state}">{subjectStateLabel(subject.state)}</span>
|
||||
<h2>{subject.role}</h2>
|
||||
<code title={subject.id}>{subject.id}</code>
|
||||
</div>
|
||||
<dl class="subject-meta">
|
||||
<div><dt>Store revision</dt><dd>{subject.store_revision}</dd></div>
|
||||
<div><dt>Updated</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div>
|
||||
<div><dt>Worker</dt><dd>{subject.current_worker?.display_name ?? 'None'}</dd></div>
|
||||
<div>
|
||||
<dt>Worker connection</dt>
|
||||
<dd>
|
||||
<strong>{subject.current_worker ? 'Connected' : 'Not connected'}</strong>
|
||||
{#if subject.current_worker}<span>{subject.current_worker.display_name}</span>{/if}
|
||||
</dd>
|
||||
</div>
|
||||
<div><dt>Last changed</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div>
|
||||
</dl>
|
||||
</a>
|
||||
{/each}
|
||||
@@ -426,8 +435,8 @@
|
||||
|
||||
.subject-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -441,12 +450,19 @@
|
||||
}
|
||||
|
||||
.subject-meta dd {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
margin-top: var(--space-1);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-compact);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.subject-meta dd strong {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.memory-state {
|
||||
padding: var(--space-5) 0;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
|
||||
import { formatDate, workspaceRoute } from '#lib/workspace/api/http.ts';
|
||||
import type { SubjektivMemoryState } from '#lib/generated/memory-api.ts';
|
||||
import type {
|
||||
SubjektivMemoryState,
|
||||
SubjektivResidentSurfaceAvailability,
|
||||
} from '#lib/generated/memory-api.ts';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
@@ -36,6 +39,24 @@
|
||||
function kindLabel(kind: string): string {
|
||||
return kind.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
function surfaceLabel(availability: SubjektivResidentSurfaceAvailability): string {
|
||||
if (availability === 'ready') return snapshot?.body_md.trim() ? 'Ready to use' : 'Ready, no context';
|
||||
if (availability === 'ungenerated') return 'Not generated';
|
||||
if (availability === 'stale') return 'Needs refresh';
|
||||
return 'Generation failed';
|
||||
}
|
||||
|
||||
function surfaceSummary(availability: SubjektivResidentSurfaceAvailability): string {
|
||||
if (availability !== 'ready') return 'No current generated context is available to use.';
|
||||
return snapshot?.body_md.trim()
|
||||
? 'Current generated context is available.'
|
||||
: 'A current empty surface exists; there is no resident context to show.';
|
||||
}
|
||||
|
||||
function subjectStateLabel(state: 'active' | 'retired'): string {
|
||||
return state === 'active' ? 'Available' : 'Retired';
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -56,13 +77,50 @@
|
||||
</header>
|
||||
|
||||
{#if data.subject.data}
|
||||
<dl class="subject-facts" aria-label="Subject details">
|
||||
<div><dt>Role</dt><dd>{data.subject.data.role}</dd></div>
|
||||
<div><dt>State</dt><dd><span class="subject-state is-{data.subject.data.state}">{data.subject.data.state}</span></dd></div>
|
||||
<div><dt>Store revision</dt><dd>{data.subject.data.store_revision}</dd></div>
|
||||
<div><dt>Updated</dt><dd><time datetime={data.subject.data.updated_at}>{formatDate(data.subject.data.updated_at)}</time></dd></div>
|
||||
<div><dt>Current worker</dt><dd>{data.subject.data.current_worker?.display_name ?? 'None'}</dd></div>
|
||||
</dl>
|
||||
<section class="subject-overview" aria-label="Subject status">
|
||||
<div>
|
||||
<span>Subject status</span>
|
||||
<strong class="subject-state is-{data.subject.data.state}">{subjectStateLabel(data.subject.data.state)}</strong>
|
||||
<p>{data.subject.data.state === 'active' ? 'Available for new Worker connections.' : 'No longer available for new Worker connections.'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Worker connection</span>
|
||||
<strong>{data.subject.data.current_worker ? 'Connected' : 'Not connected'}</strong>
|
||||
<p>{data.subject.data.current_worker?.display_name ?? 'No Worker currently owns this Subject connection.'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Committed Memories</span>
|
||||
{#if data.memories.data}
|
||||
<strong>{data.cursor ? `${memories.length} on this page` : memories.length === 0 ? 'None' : 'Available'}</strong>
|
||||
<p>{memories.length} shown{data.memories.data.has_more ? ' · more available' : ''}</p>
|
||||
{:else}
|
||||
<strong>Unavailable</strong>
|
||||
<p>The current Memory list could not be read.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span>Resident context</span>
|
||||
{#if surface}
|
||||
<strong class="availability is-{surface.availability}">{surfaceLabel(surface.availability)}</strong>
|
||||
<p>{surfaceSummary(surface.availability)}</p>
|
||||
{:else}
|
||||
<strong>Unavailable</strong>
|
||||
<p>The resident context status could not be read.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="subject-technical-details">
|
||||
<summary>Technical details</summary>
|
||||
<dl>
|
||||
<div><dt>Subject ID</dt><dd><code>{data.subject.data.id}</code></dd></div>
|
||||
<div>
|
||||
<dt>Subject store revision</dt>
|
||||
<dd>{data.subject.data.store_revision}<small>Internal change number for committed Memories; not a Memory count or content-quality score.</small></dd>
|
||||
</div>
|
||||
<div><dt>Last changed</dt><dd><time datetime={data.subject.data.updated_at}>{formatDate(data.subject.data.updated_at)}</time></dd></div>
|
||||
</dl>
|
||||
</details>
|
||||
{:else if data.subject.error}
|
||||
<div class="memory-state is-error" role="alert">
|
||||
<strong>Subject details unavailable.</strong>
|
||||
@@ -75,38 +133,40 @@
|
||||
<section class="surface-section" aria-labelledby="resident-surface-heading">
|
||||
<header class="section-heading">
|
||||
<div>
|
||||
<p class="memory-eyebrow">Resident context</p>
|
||||
<h2 id="resident-surface-heading">Resident surface</h2>
|
||||
<p class="memory-eyebrow">Generated from committed Memory</p>
|
||||
<h2 id="resident-surface-heading">Resident context</h2>
|
||||
</div>
|
||||
{#if surface}
|
||||
<span class="availability is-{surface.availability}">{surface.availability}</span>
|
||||
<span class="availability is-{surface.availability}">{surfaceLabel(surface.availability)}</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if surface?.availability === 'ready' && snapshot}
|
||||
<div class="surface-meta">
|
||||
<span>Built from store revision {snapshot.built_from_store_revision}</span>
|
||||
<span>Generated <time datetime={snapshot.created_at}>{formatDate(snapshot.created_at)}</time></span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<time datetime={snapshot.created_at}>{formatDate(snapshot.created_at)}</time>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{snapshot.memory_refs.length} Memory ref{snapshot.memory_refs.length === 1 ? '' : 's'}</span>
|
||||
<span>{snapshot.memory_refs.length} referenced Memory {snapshot.memory_refs.length === 1 ? 'record' : 'records'}</span>
|
||||
</div>
|
||||
{#if snapshot.body_md.trim().length === 0}
|
||||
<div class="memory-state" role="status" data-surface-ready-empty>
|
||||
<strong>Resident surface is ready and empty.</strong>
|
||||
<p>This subject currently has no resident context to display.</p>
|
||||
<strong>Resident context is current but empty.</strong>
|
||||
<p>A generated surface exists, but it contains no context to show. Committed Memories, if any, remain listed below.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<article class="surface-document" aria-label="Resident surface">
|
||||
<article class="surface-document" aria-label="Resident context">
|
||||
<DocumentMarkdown text={snapshot.body_md} />
|
||||
</article>
|
||||
{/if}
|
||||
<details class="surface-details">
|
||||
<summary>Surface provenance</summary>
|
||||
<summary>Surface sources and diagnostics</summary>
|
||||
<dl>
|
||||
<div><dt>Snapshot id</dt><dd><code>{snapshot.snapshot_id}</code></dd></div>
|
||||
<div><dt>Snapshot ID</dt><dd><code>{snapshot.snapshot_id}</code></dd></div>
|
||||
<div>
|
||||
<dt>Memory refs</dt>
|
||||
<dt>Generated from Subject revision</dt>
|
||||
<dd>{snapshot.built_from_store_revision}<small>The Subject change number used for this surface, distinct from each Memory’s revision.</small></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Referenced Memory revisions</dt>
|
||||
<dd>
|
||||
{#if snapshot.memory_refs.length === 0}
|
||||
None
|
||||
@@ -123,26 +183,26 @@
|
||||
</details>
|
||||
{:else if surface?.availability === 'ungenerated'}
|
||||
<div class="memory-state" role="status">
|
||||
<strong>Resident surface has not been generated.</strong>
|
||||
<p>Committed Memories remain available below.</p>
|
||||
<strong>Resident context has not been generated.</strong>
|
||||
<p>Committed Memories, if any, remain available below.</p>
|
||||
</div>
|
||||
{:else if surface?.availability === 'stale'}
|
||||
<div class="memory-state is-warning" role="status">
|
||||
<strong>Resident surface is stale.</strong>
|
||||
<p>The latest committed Memories are not represented by a publishable snapshot.</p>
|
||||
<strong>Resident context needs to be refreshed.</strong>
|
||||
<p>The latest committed Memories are not represented by a current surface, so no generated context is shown.</p>
|
||||
</div>
|
||||
{:else if surface?.availability === 'failed'}
|
||||
<div class="memory-state is-error" role="alert">
|
||||
<strong>Resident surface generation failed.</strong>
|
||||
<p>No failed or partial snapshot is displayed.</p>
|
||||
<strong>Resident context generation failed.</strong>
|
||||
<p>No failed or partial surface is shown. Committed Memories remain available below.</p>
|
||||
</div>
|
||||
{:else if data.surface.error}
|
||||
<div class="memory-state is-error" role="alert">
|
||||
<strong>Resident surface unavailable.</strong>
|
||||
<strong>Resident context status unavailable.</strong>
|
||||
<p>{data.surface.error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="memory-state" role="status"><p>Resident surface data is unavailable.</p></div>
|
||||
<div class="memory-state" role="status"><p>Resident context status is unavailable.</p></div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -152,14 +212,14 @@
|
||||
<p class="memory-eyebrow">Committed records</p>
|
||||
<h2 id="current-memories-heading">Current Memories</h2>
|
||||
</div>
|
||||
{#if data.memories.data}<span class="memory-count">{memories.length}{data.memories.data.has_more ? '+' : ''}</span>{/if}
|
||||
{#if data.memories.data}<span class="memory-count">{memories.length} shown{data.memories.data.has_more ? ' · more available' : ''}</span>{/if}
|
||||
</header>
|
||||
|
||||
{#if data.memories.data}
|
||||
{#if memories.length === 0}
|
||||
<div class="memory-state" role="status" data-memories-empty>
|
||||
<strong>No committed Memories.</strong>
|
||||
<p>This subject has no current Memory revisions.</p>
|
||||
<strong>{data.cursor ? 'No committed Memories on this page.' : 'No committed Memories yet.'}</strong>
|
||||
<p>{data.cursor ? 'Return to the first page to review earlier records.' : 'This Subject has no current Memory records.'}</p>
|
||||
{#if data.cursor}<p><a href={memoryPageHref()}>Return to the first page</a></p>{/if}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -175,9 +235,9 @@
|
||||
<p>{memory.excerpt || 'No excerpt.'}</p>
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>Memory id</dt><dd><code title={memory.id}>{memory.id}</code></dd></div>
|
||||
<div><dt>Revision</dt><dd>{memory.revision}</dd></div>
|
||||
<div><dt>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
|
||||
<div><dt>Memory ID</dt><dd><code title={memory.id}>{memory.id}</code></dd></div>
|
||||
<div><dt>Memory revision</dt><dd>{memory.revision}</dd></div>
|
||||
<div><dt>Last changed</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
|
||||
</dl>
|
||||
</a>
|
||||
{/each}
|
||||
@@ -270,25 +330,79 @@
|
||||
font-size: var(--font-size-compact);
|
||||
}
|
||||
|
||||
.subject-facts {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
.subject-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
background: var(--bg-raised);
|
||||
border-radius: var(--radius-soft);
|
||||
}
|
||||
|
||||
.subject-facts > div {
|
||||
display: block;
|
||||
.subject-overview > div {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subject-facts dt {
|
||||
white-space: normal;
|
||||
.subject-overview span,
|
||||
.subject-overview p,
|
||||
.subject-technical-details summary,
|
||||
.subject-technical-details small,
|
||||
.surface-details small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-compact);
|
||||
}
|
||||
|
||||
.subject-facts dd {
|
||||
margin-top: var(--space-1);
|
||||
color: var(--text);
|
||||
.subject-overview strong {
|
||||
color: var(--text-strong);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.subject-overview p {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.subject-technical-details {
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.subject-technical-details summary {
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.subject-technical-details dl,
|
||||
.surface-details dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.subject-technical-details dl > div,
|
||||
.surface-details dl > div {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subject-technical-details dd,
|
||||
.surface-details dd {
|
||||
margin-top: var(--space-1);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.subject-technical-details small,
|
||||
.surface-details small {
|
||||
display: block;
|
||||
margin-top: var(--space-1);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.subject-state,
|
||||
.availability,
|
||||
.memory-pill,
|
||||
@@ -455,7 +569,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.subject-facts {
|
||||
.subject-overview {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -472,10 +586,16 @@
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.subject-facts {
|
||||
.subject-overview,
|
||||
.subject-technical-details dl,
|
||||
.surface-details dl {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.subject-overview {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.memory-row {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
+20
-9
@@ -63,6 +63,11 @@
|
||||
return value.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
function titleLabel(value: string): string {
|
||||
const normalized = label(value);
|
||||
return normalized[0].toUpperCase() + normalized.slice(1);
|
||||
}
|
||||
|
||||
function rangeLabel(range: [number, number] | null | undefined): string {
|
||||
return range ? `${range[0]}–${range[1]}` : 'None';
|
||||
}
|
||||
@@ -98,8 +103,8 @@
|
||||
<header class="memory-page-header">
|
||||
<div>
|
||||
<div class="pill-row">
|
||||
<span class="memory-pill is-{memory.state}">{memory.state}</span>
|
||||
<span class="memory-kind">{label(memory.kind)}</span>
|
||||
<span class="memory-pill is-{memory.state}">{titleLabel(memory.state)}</span>
|
||||
<span class="memory-kind">{titleLabel(memory.kind)}</span>
|
||||
{#if memory.revision !== memory.current_revision}<span class="historical-pill">Historical revision</span>{/if}
|
||||
</div>
|
||||
<h1 id="memory-detail-heading">{memory.claim}</h1>
|
||||
@@ -109,11 +114,11 @@
|
||||
</header>
|
||||
|
||||
<dl class="memory-facts" aria-label="Memory details">
|
||||
<div><dt>Revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
|
||||
<div><dt>State</dt><dd>{memory.state}</dd></div>
|
||||
<div><dt>Kind</dt><dd>{label(memory.kind)}</dd></div>
|
||||
<div><dt>Memory revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
|
||||
<div><dt>Status</dt><dd>{titleLabel(memory.state)}</dd></div>
|
||||
<div><dt>Type</dt><dd>{titleLabel(memory.kind)}</dd></div>
|
||||
<div><dt>Created</dt><dd><time datetime={memory.created_at}>{formatDate(memory.created_at)}</time></dd></div>
|
||||
<div><dt>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
|
||||
<div><dt>Last changed</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
|
||||
</dl>
|
||||
|
||||
<section class="detail-section memory-body-section" aria-labelledby="memory-body-heading">
|
||||
@@ -140,7 +145,7 @@
|
||||
<section class="detail-section provenance-section" aria-labelledby="provenance-heading">
|
||||
<header class="section-heading">
|
||||
<div><p class="memory-eyebrow">Audit context</p><h2 id="provenance-heading">Candidate provenance</h2></div>
|
||||
<span>{memory.source_candidates.length} candidate{memory.source_candidates.length === 1 ? '' : 's'}</span>
|
||||
<span>{memory.source_candidates.length} shown</span>
|
||||
</header>
|
||||
|
||||
{#if memory.source_candidates.length === 0}
|
||||
@@ -226,7 +231,11 @@
|
||||
</section>
|
||||
|
||||
<section class="detail-section revision-section" aria-labelledby="revision-heading">
|
||||
<header class="section-heading"><div><p class="memory-eyebrow">Immutable record</p><h2 id="revision-heading">Revision history</h2></div>{#if data.revisions.data}<span>{revisions.length}{data.revisions.data.has_more ? '+' : ''}</span>{/if}</header>
|
||||
<header class="section-heading">
|
||||
<div><p class="memory-eyebrow">Immutable record</p><h2 id="revision-heading">Revision history</h2></div>
|
||||
{#if data.revisions.data}<span>{revisions.length} shown{data.revisions.data.has_more ? ' · more available' : ''}</span>{/if}
|
||||
</header>
|
||||
<p class="section-intro">These are versions of this Memory record, separate from the Subject’s internal update number.</p>
|
||||
{#if data.revisions.data}
|
||||
{#if revisions.length === 0}
|
||||
<div>
|
||||
@@ -238,7 +247,7 @@
|
||||
{#each revisions as revision (revision.revision)}
|
||||
<li class:current={revision.revision === memory.revision}>
|
||||
<a href={revisionHref(revision.revision)} aria-current={revision.revision === memory.revision ? 'page' : undefined}>
|
||||
<div><strong>Revision {revision.revision}</strong><span class="memory-pill is-{revision.state}">{revision.state}</span><span class="memory-kind">{label(revision.kind)}</span></div>
|
||||
<div><strong>Memory revision {revision.revision}</strong><span class="memory-pill is-{revision.state}">{titleLabel(revision.state)}</span><span class="memory-kind">{titleLabel(revision.kind)}</span></div>
|
||||
<p>{revision.claim}</p>
|
||||
<small>{revision.change_reason} · <time datetime={revision.updated_at}>{formatDate(revision.updated_at)}</time></small>
|
||||
</a>
|
||||
@@ -386,6 +395,7 @@
|
||||
}
|
||||
|
||||
.memory-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
@@ -414,6 +424,7 @@
|
||||
.why-useful,
|
||||
.staleness,
|
||||
.change-reason,
|
||||
.section-intro,
|
||||
.empty-copy,
|
||||
.bounded-note {
|
||||
margin: 0;
|
||||
|
||||
@@ -73,6 +73,9 @@ test("renders the subject index as the Memory product entry", () => {
|
||||
expect(screen.getByRole("link", { name: /Release coordinator/ })).not
|
||||
.toBeNull();
|
||||
expect(screen.getByText("subject-1")).not.toBeNull();
|
||||
expect(screen.getByText("Not connected")).not.toBeNull();
|
||||
expect(screen.queryByText("Store revision", { exact: true })).toBeNull();
|
||||
expect(screen.getByText("1 shown · more available")).not.toBeNull();
|
||||
expect(screen.getByRole("link", { name: "First page" }).getAttribute("href"))
|
||||
.toBe("/w/workspace-1/memory");
|
||||
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
|
||||
@@ -214,7 +217,10 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
|
||||
workspaceId: "workspace-1",
|
||||
subjectId: "subject-1",
|
||||
cursor: "memory-current",
|
||||
subject: result(subject()),
|
||||
subject: result({
|
||||
...subject(),
|
||||
current_worker: { display_name: "Release Worker" },
|
||||
}),
|
||||
surface: result({
|
||||
subject_id: "subject-1",
|
||||
availability: "ready" as const,
|
||||
@@ -267,9 +273,15 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
|
||||
expect(screen.getByRole("heading", { name: "Resident context", level: 1 }))
|
||||
.not.toBeNull();
|
||||
});
|
||||
expect(screen.getByRole("article", { name: "Resident surface" })).not
|
||||
expect(screen.getByRole("article", { name: "Resident context" })).not
|
||||
.toBeNull();
|
||||
expect(screen.getAllByText("active", { exact: true }).length).toBeGreaterThan(
|
||||
expect(screen.getByText("Connected", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("Release Worker", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("3 on this page", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("Subject store revision", { exact: true })).not
|
||||
.toBeNull();
|
||||
expect(screen.getByText(/not a Memory count/)).not.toBeNull();
|
||||
expect(screen.getAllByText("Active", { exact: true }).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(screen.getByText("Resolved", { exact: true })).not.toBeNull();
|
||||
@@ -280,6 +292,34 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
|
||||
.toContain("cursor=memory-next");
|
||||
});
|
||||
|
||||
test("candidate-only Subjects show no committed Memories, not an inferred candidate count", () => {
|
||||
// The store regression candidate_decisions_reject_conflicts_and_roll_back_partial_writes
|
||||
// stages a candidate without committing a Memory. Its public read projection is
|
||||
// the same empty committed list as a new Subject; candidates are not a page input.
|
||||
render(SubjectPage, {
|
||||
data: {
|
||||
workspaceId: "workspace-1",
|
||||
subjectId: "subject-1",
|
||||
cursor: null,
|
||||
subject: result({ ...subject(), store_revision: 0 }),
|
||||
surface: result({ subject_id: "subject-1", availability: "ungenerated" }),
|
||||
memories: result({ limit: 100, items: [], has_more: false }),
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(screen.getByText("None", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("No committed Memories yet.")).not.toBeNull();
|
||||
expect(screen.getByText("Not connected", { exact: true })).not.toBeNull();
|
||||
expect(screen.getAllByText("Not generated", { exact: true })).toHaveLength(2);
|
||||
expect(screen.queryByRole("article", { name: "Resident context" }))
|
||||
.toBeNull();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
/candidate count|all Memories lost|automatically updating/i,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("distinguishes ready-empty, stale, failed, unavailable, and request error surfaces", async () => {
|
||||
const base = {
|
||||
workspaceId: "workspace-1",
|
||||
@@ -303,8 +343,25 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
expect(screen.getByText("Resident surface is ready and empty.")).not
|
||||
expect(screen.getByText("Resident context is current but empty.")).not
|
||||
.toBeNull();
|
||||
expect(screen.getByText(/current empty surface exists/)).not.toBeNull();
|
||||
expect(screen.getByText("No committed Memories yet.")).not.toBeNull();
|
||||
|
||||
await view.rerender(
|
||||
{
|
||||
data: {
|
||||
...base,
|
||||
surface: result({
|
||||
subject_id: "subject-1",
|
||||
availability: "ungenerated" as const,
|
||||
}),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
expect(screen.getByText("Resident context has not been generated.")).not
|
||||
.toBeNull();
|
||||
expect(screen.getAllByText("Not generated", { exact: true })).toHaveLength(2);
|
||||
|
||||
await view.rerender(
|
||||
{
|
||||
@@ -317,7 +374,8 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
expect(screen.getByText("Resident surface is stale.")).not.toBeNull();
|
||||
expect(screen.getByText("Resident context needs to be refreshed.")).not
|
||||
.toBeNull();
|
||||
|
||||
await view.rerender(
|
||||
{
|
||||
@@ -333,7 +391,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
|
||||
expect(screen.getByRole("alert").textContent).toContain("generation failed");
|
||||
|
||||
await view.rerender({ data: { ...base, surface: result(null) } } as never);
|
||||
expect(screen.getByText("Resident surface data is unavailable.")).not
|
||||
expect(screen.getByText("Resident context status is unavailable.")).not
|
||||
.toBeNull();
|
||||
|
||||
await view.rerender(
|
||||
@@ -342,7 +400,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
|
||||
} as never,
|
||||
);
|
||||
expect(screen.getByRole("alert").textContent).toContain(
|
||||
"Resident surface unavailable",
|
||||
"Resident context status unavailable",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -61,9 +61,7 @@ async function assertSubjectCreateRejects(
|
||||
assertEquals(error.status, expectedStatus);
|
||||
if (!error.message.includes(expectedMessage)) {
|
||||
throw new Error(
|
||||
`expected ${JSON.stringify(error.message)} to include ${
|
||||
JSON.stringify(expectedMessage)
|
||||
}`,
|
||||
`expected ${JSON.stringify(error.message)} to include ${JSON.stringify(expectedMessage)}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -82,6 +80,30 @@ function subject(id = "subject-1") {
|
||||
};
|
||||
}
|
||||
|
||||
function currentWorker() {
|
||||
return {
|
||||
runtime_id: "runtime-1",
|
||||
worker_id: "worker-1",
|
||||
host_id: "host-1",
|
||||
display_name: "Release Worker",
|
||||
label: "Release Worker",
|
||||
profile: "builtin:companion",
|
||||
singleton_key: "subjektiv:subject-1",
|
||||
tags: [],
|
||||
workspace: {
|
||||
visibility: "workspace",
|
||||
identity: "workspace-1",
|
||||
workspace_id: "workspace-1",
|
||||
},
|
||||
state: "idle",
|
||||
last_seen_at: "2026-09-02T00:00:00Z",
|
||||
pinned: false,
|
||||
retention_state: "retained",
|
||||
implementation: { kind: "embedded", display_hint: "Embedded" },
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
function queryItem(id = "memory-1") {
|
||||
return {
|
||||
id,
|
||||
@@ -187,14 +209,35 @@ Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bo
|
||||
() =>
|
||||
parseSubjektivSubjectListResponse({
|
||||
limit: 100,
|
||||
items: Array.from({ length: 101 }, (_, index) =>
|
||||
subject(`subject-${index}`)),
|
||||
items: Array.from({ length: 101 }, (_, index) => subject(`subject-${index}`)),
|
||||
has_more: false,
|
||||
}),
|
||||
"bounded array",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Subject parser preserves live Worker connection state", () => {
|
||||
assertEquals(
|
||||
parseSubjektivSubjectResponse({
|
||||
...subject(),
|
||||
current_worker: currentWorker(),
|
||||
}),
|
||||
{ ...subject(), current_worker: currentWorker() },
|
||||
);
|
||||
assertEquals(
|
||||
parseSubjektivSubjectResponse({ ...subject(), current_worker: null }),
|
||||
{ ...subject(), current_worker: null },
|
||||
);
|
||||
assertThrows(
|
||||
() =>
|
||||
parseSubjektivSubjectResponse({
|
||||
...subject(),
|
||||
current_worker: { display_name: "Incomplete Worker" },
|
||||
}),
|
||||
"current worker.runtime_id",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Subject list parser preserves the exact bounded response", () => {
|
||||
const response = { limit: 100, items: [subject()], has_more: false };
|
||||
assertEquals(parseSubjektivSubjectListResponse(response), response);
|
||||
@@ -328,8 +371,7 @@ Deno.test("Subject create client distinguishes rejected and unknown outcomes wit
|
||||
throw new TypeError("connection reset");
|
||||
}) as typeof fetch;
|
||||
await assertSubjectCreateRejects(
|
||||
() =>
|
||||
createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
|
||||
() => createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
|
||||
"unknown_outcome",
|
||||
"may have been created",
|
||||
);
|
||||
@@ -433,8 +475,7 @@ Deno.test("Current Memory list parser rejects unknown variants, unsafe integers,
|
||||
assertThrows(
|
||||
() =>
|
||||
parseSubjektivMemoryQueryResponse({
|
||||
items: Array.from({ length: 101 }, (_, index) =>
|
||||
queryItem(`memory-${index}`)),
|
||||
items: Array.from({ length: 101 }, (_, index) => queryItem(`memory-${index}`)),
|
||||
has_more: false,
|
||||
}),
|
||||
"bounded array",
|
||||
|
||||
Reference in New Issue
Block a user