Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/IMPLEMENTATION_MASTERPLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Last Updated: 2026-09-10

Comparison attribution follow-through (#2808): retain the resolved Auto appearance in the existing portable theme label and exclude known non-shipped test inputs from the frontend fingerprint. Preserve legacy checksums/file semantics and prove live light/night switching, export/import and HTTP LAN compatibility together.

Original-source portability continuation (#2808): hold a store-owned read snapshot across the five source-storage export sections, with deferred SQLite transactions and scoped disposal before later export writes. Prove both account-export routes against a concurrent committed upload and written version. Whole-account point-in-time consistency and restore acceptance remain separate.

Studio/Classic planning continuity (#2808): share existing Focus resume on Classic Home and use the existing calendar-day utilities for plan date controls and card deadlines. Browser acceptance includes a western timezone, Today filtering, both Classic renderers and unchanged board-card data. Failed-plan metadata and accepted-navigation focus timestamps remain separate follow-through.
Expand Down
2 changes: 2 additions & 0 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Last Updated: 2026-09-10

Comparison appearance continuation (#2808): Auto observations retain both the selected mode and the resolved light/night appearance at submission. Existing version-2/3 files keep their original attribution; no historical appearance is invented. Frontend input fingerprints exclude repository test-only sources and build proof scripts, while runtime sources, public assets, build plugins and resolved build options remain attributed. This keeps manual comparisons coherent across proof-only edits without claiming randomized or statistical results.

Source-storage export continuation (#2808): buffered and streamed account exports now hold one deferred SQLite read snapshot across blob objects, references, chunks, representations and audio-answer rows. Concurrent WAL uploads can commit without reserving the writer for the duration of the export; all five sections retain the earlier view until disposal. This is source-storage consistency, not a claim of a single snapshot across every account-export section or a tested restore workflow.

Planning continuity follow-through (#2808): Classic Home now offers the same private last-worked Focus resume as the other experiences, in both Paper/Grove and Legacy rendering. Personal-plan due dates use canonical calendar-day formatting; Today and tomorrow use the local calendar date without changing card deadlines. This reduces the work needed to resume a thought after switching versions.
Expand Down
7 changes: 7 additions & 0 deletions docs/product/WORKSPACE_OVERHAUL_VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,10 @@ PR #2866 now also contains the reviewed source interaction, board overlay, libra
Sixteen selected Chromium journeys have passing evidence. The initial combined batch passed fifteen and found one ambiguous memory-title locator; using the checkbox role fixed that test, and both complete contextual/source specs then passed. Coverage includes four experiences, both board renderers, Los Angeles calendar dates, Classic resume, 375 px accessibility/overflow, source permissions, exact original audio, explicit Review/Apply and three production audio-policy cases. This is not a claim that the initial batch was entirely green.

Two comparison identity checks, two audio-policy checks, eleven failure-ledger projection tests, documentation links and GitHub governance pass. A bounded fresh-context integration review found no HIGH/CRITICAL interaction defect. Temporary browser services are stopped; all databases are synthetic. Hosted exact-head qualification remains the merge gate. The wider #2808 scope, physical microphones/devices, live processors, restoration and the existing human decisions remain open.

## Comparison appearance attribution (2026-09-10)

Auto observations use the existing version-3 theme string to retain both selected mode and resolved appearance at submission: auto (paper) or auto (paper-night). Old auto labels remain unchanged and separate; no version-2 checksum input, file schema or historic identity changes. The UI explains the submission-time scope and asks the observer to note any appearance changes during the task.

The HTTP LAN compatibility Chromium journey records both live color-scheme settings, confirms the body theme, exports those labels alongside a legacy auto observation, checks three separate result rows, reloads and imports all three without SubtleCrypto or randomUUID. It passes in 14.6 seconds. Node fingerprint checks verify that src/tests and build/*.node-check.mjs changes leave product attribution stable, while runtime source, public assets, build plugin code and build configuration change it. Full frontend/build results belong to the continuation PR; there is no telemetry, randomized assignment or statistical conclusion.
Final local comparison-attribution gate: 6,377 frontend tests passed with three existing skips across 412 files. Production build/typecheck, two Node identity checks, the 14.6-second Chromium journey, documentation links and GitHub governance pass. Independent bounded Luna review is CLEAN. The test-only fingerprint exclusions are exact repository paths; this is still an input fingerprint, not a byte-for-byte bundle hash or proof of the remote server version. Temporary services are stopped; hosted checks remain separate.
11 changes: 10 additions & 1 deletion frontend/taskdeck-web/build/frontendIdentity.node-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ test('frontend fingerprint is repeatable and changes with source, public assets
const baseline = fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' })
assert.match(baseline, /^sha256:[a-f0-9]{64}$/)
assert.equal(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }), baseline)
mkdirSync(join(root, 'src/tests'))
writeFileSync(join(root, 'src/tests/new.spec.ts'), 'new proof, same product')
writeFileSync(join(root, 'build/example.node-check.mjs'), 'build proof')
assert.equal(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }), baseline)
writeFileSync(join(root, 'src/tests/new.spec.ts'), 'updated proof')
assert.equal(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }), baseline)
assert.notEqual(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/other-api' }), baseline)
assert.notEqual(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }, { base: '/Taskdeck/' }), baseline)
assert.notEqual(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }, { minify: false }), baseline)
writeFileSync(join(root, 'src/main.ts'), 'changed content')
const changedSource = fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' })
assert.notEqual(changedSource, baseline)
writeFileSync(join(root, 'public/theme.css'), 'changed theme')
assert.notEqual(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }), changedSource)
const changedAssets = fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' })
assert.notEqual(changedAssets, changedSource)
writeFileSync(join(root, 'build/runtime-plugin.ts'), 'plugin affecting output')
assert.notEqual(fingerprintFrontend(root, 'production', { VITE_API_BASE_URL: '/api' }), changedAssets)
})
2 changes: 2 additions & 0 deletions frontend/taskdeck-web/build/frontendIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export function fingerprintFrontend(root: string, mode: string, publicEnvironmen
function directory(relative: string) {
for (const entry of readdirSync(join(root, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
const name = `${relative}/${entry.name}`
// These repository-owned proof inputs are not shipped in the frontend bundle.
if (name === 'src/tests' || (relative === 'build' && entry.isFile() && entry.name.endsWith('.node-check.mjs'))) continue
if (entry.isDirectory()) directory(name)
else if (entry.isFile()) add(name, readFileSync(join(root, name)))
else throw new Error(`Unsupported frontend input: ${name}`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useWorkspaceLayoutStore } from '../../store/workspaceLayoutStore'
import { usePaperThemeStore } from '../../store/paperThemeStore'
import { resolveBodyClass, usePaperThemeStore } from '../../store/paperThemeStore'
import {
useWorkspaceExperimentStore,
WORKSPACE_COMPARISON_SCENARIOS,
Expand Down Expand Up @@ -38,7 +38,7 @@ function record() {
saved.value = experiment.record({
experience: layout.experience,
presentation: layout.presentation,
theme: theme.mode,
theme: theme.mode === 'auto' ? `auto (${resolveBodyClass('auto')})` : theme.mode,
build: version.value,
scenario: scenario.value,
completionOutcome: completionOutcome.value,
Expand Down Expand Up @@ -77,6 +77,7 @@ function download() {
<form class="experience-lab__notes" @submit.prevent="record">
<h2>Keep an observation</h2><p>Recording {{ layout.experience }} / {{ layout.presentation }} / {{ theme.mode }} · backend version {{ displayVersion || 'unavailable' }}. Notes stay in this session; export them before reloading or signing out. Import the file later to compare releases.</p>
<details><summary>Frontend build attribution</summary><p>{{ frontendBuildIdentity || 'Development or unstamped build: frontend attribution unavailable.' }}</p></details>
<p v-if="theme.mode === 'auto'">Auto records the light or night appearance when you submit. If it changed during your task, mention that in your note.</p>
<label for="experience-scenario">What scenario did you try?</label><select id="experience-scenario" v-model="scenario" required><option :value="null" disabled>Select a scenario</option><option v-for="item in WORKSPACE_COMPARISON_SCENARIOS" :key="item.id" :value="item.id">{{ item.label }}</option></select>
<label for="experience-outcome">What happened?</label><select id="experience-outcome" v-model="completionOutcome" required><option :value="null" disabled>Select an outcome</option><option v-for="item in WORKSPACE_COMPLETION_OUTCOMES" :key="item.id" :value="item.id">{{ item.label }}</option></select>
<label for="experience-ease">How easy was it to continue your work? (optional)</label><select id="experience-ease" v-model="ease"><option :value="null">Leave unrated</option><option :value="1">1 — Difficult</option><option :value="2">2 — Some friction</option><option :value="3">3 — Reasonable</option><option :value="4">4 — Easy</option><option :value="5">5 — Effortless</option></select>
Expand Down
20 changes: 13 additions & 7 deletions frontend/taskdeck-web/tests/e2e/comparison-compatibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ test('comparison files work with the crypto APIs available on HTTP LAN', async (
await page.addInitScript(() => {
Object.defineProperty(globalThis.crypto, 'randomUUID', { value: undefined, configurable: true })
Object.defineProperty(globalThis.crypto, 'subtle', { value: undefined, configurable: true })
localStorage.setItem('td.paper.mode.v2', 'auto')
})
await page.goto('/workspace/experiences')
await page.getByLabel('What scenario did you try?').selectOption('resume-thinking')
await page.getByLabel('What happened?').selectOption('completed')
await page.getByRole('button', { name: 'Record observation', exact: true }).click()
await expect(page.getByText('Observation recorded.', { exact: true })).toBeVisible()
for (const colorScheme of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme })
await expect(page.locator('body')).toHaveClass(colorScheme === 'light' ? /\bpaper\b/ : /\bpaper-night\b/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the complete light body class

When Auto should render light, /\bpaper\b/ also matches paper-night because the hyphen creates a word boundary. The new E2E can therefore pass if the visible theme remains night (or both classes remain) while the exported observation records auto (paper), so it does not prove that recorded attribution matches the rendered appearance; assert a whitespace-delimited paper class and the absence of paper-night.

Useful? React with 👍 / 👎.

await page.getByLabel('What scenario did you try?').selectOption('resume-thinking')
await page.getByLabel('What happened?').selectOption('completed')
await page.getByRole('button', { name: 'Record observation', exact: true }).click()
await expect(page.getByText('Observation recorded.', { exact: true })).toBeVisible()
}
const legacy = JSON.stringify({ kind: 'taskdeck-workspace-comparison', version: 2, trials: [{
experience: 'classic', presentation: 'studio', theme: 'grove', build: null, scenario: 'resume-thinking',
experience: 'classic', presentation: 'studio', theme: 'auto', build: null, scenario: 'resume-thinking',
completionOutcome: 'completed', recordedAt: '2026-09-01T00:00:00Z', ease: null, note: 'Retained LAN observation',
}] })
const input = page.getByLabel('Import saved observations', { exact: true })
Expand All @@ -25,8 +30,9 @@ test('comparison files work with the crypto APIs available on HTTP LAN', async (
await page.getByRole('button', { name: 'Export observations', exact: true }).click()
const download = await downloading; const stream = await download.createReadStream()
let retained = ''; for await (const chunk of stream!) retained += chunk.toString()
expect(JSON.parse(retained).trials).toHaveLength(2)
expect(JSON.parse(retained).trials.map((trial: { theme: string }) => trial.theme)).toEqual(['auto (paper)', 'auto (paper-night)', 'auto'])
await expect(page.getByRole('region', { name: 'Compare observations across releases' }).locator('tbody tr')).toHaveCount(3)
await page.reload()
await input.setInputFiles({ name: 'retained.json', mimeType: 'application/json', buffer: Buffer.from(retained) })
await expect(page.getByText('Imported 2 observations. Exact duplicates were skipped.', { exact: true })).toBeVisible()
await expect(page.getByText('Imported 3 observations. Exact duplicates were skipped.', { exact: true })).toBeVisible()
})
Loading