diff --git a/AGENT_COORDINATION.md b/AGENT_COORDINATION.md index c9d904f62..6f61b33df 100644 --- a/AGENT_COORDINATION.md +++ b/AGENT_COORDINATION.md @@ -57,6 +57,11 @@ Server Error) for an unknown slug. ## Active claims (who is editing what RIGHT NOW) +- **2026-07-29 · Codex `/root` →** `apps/web/src/features/entities/components/notebook/EntityNotebookLive.tsx`, + its mount/tests, and `docs/design/note-surface/*` · complete the actual Live notebook + Cmd/Ctrl+E capture path and rebuild the Mobbin/Mew/Roam evidence chain · branch + `codex/note-surface-complete` / PR #601. No backend or Convex deploy. + > **STANDARD-TREE MIGRATION (2026-07-19, feat/standard-tree-migration): repo paths moved.** > `convex/` → `backend/convex/` (convex.json `functions` added; function identifiers unchanged), > `src/` + `index.html` → `apps/web/` (`@convex` alias replaces relative `../convex` imports), diff --git a/CHANGELOG/README.md b/CHANGELOG/README.md index 22f05fe9d..ac718f3a1 100644 --- a/CHANGELOG/README.md +++ b/CHANGELOG/README.md @@ -15,6 +15,8 @@ This directory contains append-only per-surface changelog lanes. Each lane recor ### Components +- [`components/entity-notebook-surface.md`](components/entity-notebook-surface.md) - Entity notebook composition and authority propagation. +- [`components/entity-notebook-live.md`](components/entity-notebook-live.md) - Live block stream, capture shortcut, and fail-closed editing. - [`components/fast-agent-panel.md`](components/fast-agent-panel.md) — FastAgentPanel AI Elements cutovers and preserved behavior seams. ### Integrations diff --git a/CHANGELOG/components/entity-notebook-live.md b/CHANGELOG/components/entity-notebook-live.md new file mode 100644 index 000000000..0cc00526a --- /dev/null +++ b/CHANGELOG/components/entity-notebook-live.md @@ -0,0 +1,22 @@ +# Entity notebook Live surface + +Append-only lane for the block-stream editor, its read/edit transition, capture +shortcut, and fail-closed notebook authority behavior. Newest entries first. + +## 2026-07-29 - Arm desktop capture from read mode + +Make Cmd/Ctrl+E enter edit mode and focus the first editable Live block. Empty +notebooks create exactly one block, while active text inputs, repeated shortcuts, +shared routes, and notebooks containing only read-only blocks remain protected. + +**PR / canonical main commit**: PENDING #601 MAIN SHA / FINAL QA. + +**Evidence state**: +- Source: pending on PR #601. +- Checks: TypeScript passed; the focused EntityNotebookSurface and EntityNotebookLive scenario pack passed 15/15; production build passed; the NodeKit reference corpus gate passed 10/10 records with 21 facts and 53 citations. +- Visual proof: pending authenticated Chrome capture; failing-before and passing-after behavior receipts are in `evidence/note-surface-live-capture/`. +- Preview: pending exact-head deployment after this update. +- Production live: not recorded. + +**Author**: Homen Shum + Codex. +**Touches**: [`entity-notebook-surface.md`](entity-notebook-surface.md). diff --git a/CHANGELOG/components/entity-notebook-surface.md b/CHANGELOG/components/entity-notebook-surface.md new file mode 100644 index 000000000..ba78817d1 --- /dev/null +++ b/CHANGELOG/components/entity-notebook-surface.md @@ -0,0 +1,24 @@ +# Entity notebook surface + +Append-only lane for the entity notebook composition boundary that selects the +fallback or Live notebook and passes workspace authority into the mounted +surface. Newest entries first. + +## 2026-07-29 - Preserve write authority across read mode + +Pass the notebook's underlying write authority into the Live surface instead of +discarding it when the route renders in read mode. The Live component now owns +the temporary read-only mask, allowing an authorized reader to use the capture +shortcut without granting write access to shared or read-only notebooks. + +**PR / canonical main commit**: PENDING #601 MAIN SHA / FINAL QA. + +**Evidence state**: +- Source: pending on PR #601. +- Checks: TypeScript passed; the focused EntityNotebookSurface and EntityNotebookLive scenario pack passed 15/15; production build passed. +- Visual proof: pending authenticated Chrome capture; component focus behavior is recorded in `evidence/note-surface-live-capture/after.txt`. +- Preview: pending exact-head deployment after this update. +- Production live: not recorded. + +**Author**: Homen Shum + Codex. +**Touches**: [`entity-notebook-live.md`](entity-notebook-live.md). diff --git a/apps/web/src/features/entities/components/EntityNoteEditor.tsx b/apps/web/src/features/entities/components/EntityNoteEditor.tsx index a1494c24e..6d378c425 100644 --- a/apps/web/src/features/entities/components/EntityNoteEditor.tsx +++ b/apps/web/src/features/entities/components/EntityNoteEditor.tsx @@ -508,12 +508,16 @@ export const EntityNoteEditor = forwardRef
+ {/* rule-single-accent-inline obs-mew-tokens/f1 + obs-mew-stream/f2: the mode + toggle is chrome, so its selected state is achromatic — the accent stays + reserved for inline meaning carriers (citations, links, tags). This also + removes the hardcoded #d97757 literals that bypassed --accent-primary. */}
}> + + Loading notebook... +
+ } + >
- Loading live notebook...}> + + Loading live notebook... + + } + >
buildEntityPath(nextSlug, shareToken); const canTraverseLinkedEntities = !shareToken; - const [routingOpen, setRoutingOpen] = useState(true); - const [planOpen, setPlanOpen] = useState(true); + // rule-stream-not-chrome (note-surface corpus): provenance is available, not + // interposed — panels default closed so the first thing after the header is + // the note stream, not chrome. + const [routingOpen, setRoutingOpen] = useState(false); + const [planOpen, setPlanOpen] = useState(false); const [sourcesOpen, setSourcesOpen] = useState(false); const citationLabelByKey = useMemo(() => { @@ -197,7 +200,8 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false return (
-
+ {/* rule-stream-not-chrome obs-mew-stream/f1: header ends with a divider, not a card */} +
@@ -217,7 +221,8 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false
-
+ {/* rule-single-accent-inline obs-mew-stream/f2: run metadata is chrome, so it stays achromatic */} +
{routingSummary}
@@ -239,16 +244,16 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false
setRoutingOpen((event.target as HTMLDetailsElement).open)} - className="mt-4 overflow-hidden rounded-xl border border-gray-200 dark:border-white/10" + className="mt-4 border-b border-gray-100 dark:border-white/[0.06]" > - +
Routing & operator context how this brief was generated
{routing.mode ? ( - + {routing.mode} {routing.reasoningEffort ? ` · ${routing.reasoningEffort} reasoning` : ""} @@ -269,9 +274,9 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false
setPlanOpen((event.target as HTMLDetailsElement).open)} - className="mt-3 overflow-hidden rounded-xl border border-gray-200 dark:border-white/10" + className="mt-1 border-b border-gray-100 dark:border-white/[0.06]" > - +
Execution plan trace @@ -292,10 +297,10 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false className="grid grid-cols-[24px_170px_1fr_auto] gap-3 border-b border-dashed border-gray-200/40 py-1.5 last:border-0 dark:border-white/[0.06]" > {step.step} - + {step.tool} {step.parallel ? ( - + parallel ) : null} @@ -331,9 +336,9 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false
setSourcesOpen((event.target as HTMLDetailsElement).open)} - className="mt-3 overflow-hidden rounded-xl border border-gray-200 dark:border-white/10" + className="mt-1 border-b border-gray-100 dark:border-white/[0.06]" > - +
Sources with confidence @@ -417,14 +422,15 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false
Linked from · {backlinks.length} place{backlinks.length === 1 ? "" : "s"}
-
+ {/* rule-stream-not-chrome obs-mew-stream/f1: backlinks are stream entries — divider separation, no cards */} +
{backlinks.map((ref: any) => canTraverseLinkedEntities ? ( ) : (
- + {item.name} {item.reason ? · {item.reason} : null}
@@ -485,14 +493,16 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false
Related entities (harness-suggested)
-
+ {/* rule-stream-not-chrome obs-mew-stream/f1 + rule-single-accent-inline obs-mew-stream/f2: + divider rows, and the suggestion glyph is chrome so it goes achromatic */} +
{snapshot.relatedEntities.map((item: any) => canTraverseLinkedEntities ? (
- + ) : (
{item.name}
@@ -513,7 +523,7 @@ export function EntityNotebookView({ entitySlug, shareToken, canOpenLive = false {item.reason ?? item.relation ?? item.entityType}
- +
) )} @@ -618,12 +628,14 @@ function BlockBody({ (block.href ? sourcesByKey.get(block.href) : undefined) ?? sourcesByKey.get(block.body) ?? (block.evidenceDomain ? sourcesByKey.get(block.evidenceDomain) : undefined); + // rule-stream-not-chrome obs-mew-stream/f1: evidence is an inline link in the + // stream, not a bordered chip — the accent icon marks it as a meaning carrier. return ( @@ -680,10 +692,13 @@ function BlockProvenance({ block }: { block: NotebookBlock }) { if (block.author !== "agent" || block.kind !== "text") return null; const items: Array<{ label: string; tone: string; title?: string }> = []; + // rule-single-accent-inline obs-mew-stream/f2: provenance chips are chrome, + // not inline meaning carriers — they stay achromatic so the accent keeps + // meaning "citation/link" on this surface. if (block.modelUsed) { items.push({ label: block.modelUsed, - tone: "text-[var(--accent-primary)] bg-[var(--accent-primary)]/10", + tone: "text-gray-500 bg-gray-500/10", title: "Model used", }); } @@ -697,7 +712,7 @@ function BlockProvenance({ block }: { block: NotebookBlock }) { if (block.costUsd != null) { items.push({ label: formatUsd(block.costUsd), - tone: "text-amber-500 bg-amber-500/10", + tone: "text-gray-500 bg-gray-500/10", title: "Estimated cost", }); } diff --git a/apps/web/src/features/entities/components/notebook/EntityNotebookLive.empty-state.test.tsx b/apps/web/src/features/entities/components/notebook/EntityNotebookLive.empty-state.test.tsx index 5d2d39ed0..5d165f689 100644 --- a/apps/web/src/features/entities/components/notebook/EntityNotebookLive.empty-state.test.tsx +++ b/apps/web/src/features/entities/components/notebook/EntityNotebookLive.empty-state.test.tsx @@ -91,25 +91,35 @@ const { vi.mock("convex/react", () => ({ usePaginatedQuery: vi.fn(() => paginatedState), useQuery: vi.fn((query: unknown) => { - if (query === api.domains.product.blocks.getEntityNotebook) return queryState.snapshot; - if (query === api.domains.product.blocks.getEntityBlockSummary) return queryState.blockSummary; - if (query === api.domains.product.blocks.listBacklinksForEntity) return queryState.backlinks; - if (query === api.domains.product.diligenceProjections.listForEntity) return []; + if (query === api.domains.product.blocks.getEntityNotebook) + return queryState.snapshot; + if (query === api.domains.product.blocks.getEntityBlockSummary) + return queryState.blockSummary; + if (query === api.domains.product.blocks.listBacklinksForEntity) + return queryState.backlinks; + if (query === api.domains.product.diligenceProjections.listForEntity) + return []; return undefined; }), useMutation: vi.fn((mutation: unknown) => { - if (mutation === api.domains.product.blocks.appendBlock) return mockAppendBlock; + if (mutation === api.domains.product.blocks.appendBlock) + return mockAppendBlock; if (mutation === api.domains.product.blocks.insertBlockBetween) { return mockInsertBlockBetween; } - if (mutation === api.domains.product.blocks.updateBlock) return mockUpdateBlock; + if (mutation === api.domains.product.blocks.updateBlock) + return mockUpdateBlock; if (mutation === api.domains.product.blocks.backfillEntityBlocks) { return mockBackfillEntityBlocks; } - if (mutation === api.domains.product.diligenceProjections.materializeForEntity) { + if ( + mutation === api.domains.product.diligenceProjections.materializeForEntity + ) { return mockMaterializeForEntity; } - if (mutation === api.domains.product.diligenceProjections.requestRefreshAndRun) { + if ( + mutation === api.domains.product.diligenceProjections.requestRefreshAndRun + ) { return mockRequestRefreshAndRun; } return vi.fn(); @@ -149,20 +159,32 @@ vi.mock("@convex-dev/prosemirror-sync/tiptap", () => ({ vi.mock("./NotebookBlockEditor", async () => { const React = await import("react"); - const renderChips = (chips: Array<{ value: string }>) => chips.map((chip) => chip.value).join(""); - const MockNotebookBlockEditor = React.forwardRef(function MockNotebookBlockEditor( - props: { - ariaLabel: string; - chips: Array<{ value: string }>; + const renderChips = (chips: Array<{ value: string }>) => + chips.map((chip) => chip.value).join(""); + const MockNotebookBlockEditor = React.forwardRef( + function MockNotebookBlockEditor( + props: { + ariaLabel: string; + chips: Array<{ value: string }>; + }, + ref: React.ForwardedRef, + ) { + const elementRef = React.useRef(null); + React.useImperativeHandle(ref, () => ({ + focus: () => elementRef.current?.focus(), + })); + return ( +
+ {renderChips(props.chips)} +
+ ); }, - _ref: React.ForwardedRef, - ) { - return ( -
- {renderChips(props.chips)} -
- ); - }); + ); return { NotebookBlockEditor: MockNotebookBlockEditor, @@ -229,6 +251,137 @@ describe("EntityNotebookLive empty live notebook", () => { ); }); + it("moves a reader into the first editable live block with Cmd+E", async () => { + paginatedState.results = [ + { + _id: "block_first", + entityId: "entity_1", + kind: "text", + authorKind: "user", + content: [{ type: "text", value: "Existing note" }], + positionInt: 1, + positionFrac: "a0", + revision: 1, + updatedAt: Date.now(), + accessMode: "edit", + }, + ]; + queryState.blockSummary = { + blockCount: 1, + userEditedCount: 1, + }; + + renderWithRouter(, { + route: "/entity/smr-thesis?view=read", + }); + + fireEvent.keyDown(window, { key: "e", metaKey: true }); + + await waitFor(() => + expect( + screen.getByRole("textbox", { name: "Block - text" }), + ).toHaveFocus(), + ); + expect(mockAppendBlock).not.toHaveBeenCalled(); + }); + + it("creates exactly one first block for two rapid capture shortcuts on an empty notebook", async () => { + renderWithRouter(, { + route: "/entity/smr-thesis?view=read", + }); + + fireEvent.keyDown(window, { key: "e", ctrlKey: true }); + fireEvent.keyDown(window, { key: "e", ctrlKey: true }); + fireEvent.keyDown(window, { key: "e", ctrlKey: true, repeat: true }); + + await waitFor(() => expect(mockAppendBlock).toHaveBeenCalledTimes(1)); + }); + + it("does not steal Cmd+E from an active text field", async () => { + renderWithRouter( + <> + + + , + { route: "/entity/smr-thesis?view=read" }, + ); + const input = screen.getByRole("textbox", { name: "Search notes" }); + input.focus(); + + fireEvent.keyDown(input, { key: "e", metaKey: true }); + + await Promise.resolve(); + expect(input).toHaveFocus(); + expect(mockAppendBlock).not.toHaveBeenCalled(); + }); + + it("does not create a block when the notebook or every existing block is read-only", async () => { + paginatedState.results = [ + { + _id: "block_read_only", + entityId: "entity_1", + kind: "text", + authorKind: "agent", + content: [{ type: "text", value: "Shared note" }], + positionInt: 1, + positionFrac: "a0", + revision: 1, + updatedAt: Date.now(), + accessMode: "read", + }, + ]; + queryState.blockSummary = { + blockCount: 1, + userEditedCount: 0, + }; + + const { unmount } = renderWithRouter( + , + { route: "/entity/smr-thesis?view=read" }, + ); + fireEvent.keyDown(window, { key: "e", metaKey: true }); + + await waitFor(() => + expect(mockToastWarning).toHaveBeenCalledWith( + "Block is read-only", + expect.stringContaining("edit this block"), + ), + ); + expect(mockAppendBlock).not.toHaveBeenCalled(); + unmount(); + + renderWithRouter( + , + { route: "/entity/smr-thesis?view=read" }, + ); + fireEvent.keyDown(window, { key: "e", metaKey: true }); + await Promise.resolve(); + expect(mockAppendBlock).not.toHaveBeenCalled(); + }); + + it("leaves the capture shortcut untouched on a shared notebook route", async () => { + renderWithRouter( + , + { route: "/share/share_read_only?view=read" }, + ); + const event = new KeyboardEvent("keydown", { + key: "e", + metaKey: true, + bubbles: true, + cancelable: true, + }); + + window.dispatchEvent(event); + + await Promise.resolve(); + expect(event.defaultPrevented).toBe(false); + expect(mockAppendBlock).not.toHaveBeenCalled(); + }); + it("hydrates the notebook from the saved brief when a reference projection exists", async () => { queryState.snapshot = { blocks: [ @@ -340,7 +493,13 @@ describe("EntityNotebookLive empty live notebook", () => { kind: "heading_2", authorKind: "agent", authorId: "system-archive", - content: [{ type: "text", value: "Supply chain AI startup Loop just raised $95M in a Series C round." }], + content: [ + { + type: "text", + value: + "Supply chain AI startup Loop just raised $95M in a Series C round.", + }, + ], positionInt: 1, positionFrac: "a0", revision: 1, @@ -368,7 +527,12 @@ describe("EntityNotebookLive empty live notebook", () => { kind: "text", authorKind: "agent", authorId: "system-archive", - content: [{ type: "text", value: "This notebook was projected from archived intelligence." }], + content: [ + { + type: "text", + value: "This notebook was projected from archived intelligence.", + }, + ], sourceRefIds: ["src_1"], positionInt: 3, positionFrac: "a2", @@ -407,7 +571,9 @@ describe("EntityNotebookLive empty live notebook", () => { userEditedCount: 0, }; - renderWithRouter(); + renderWithRouter( + , + ); const overlayHost = screen.getByTestId("notebook-diligence-overlay-host"); expect(overlayHost).toBeInTheDocument(); @@ -476,17 +642,28 @@ describe("EntityNotebookLive empty live notebook", () => { renderWithRouter(); await waitFor(() => - expect(screen.getByTestId("notebook-diligence-overlay-host")).toBeInTheDocument(), + expect( + screen.getByTestId("notebook-diligence-overlay-host"), + ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: "Accept" })); - await waitFor(() => expect(mockInsertBlockBetween).toHaveBeenCalledTimes(3)); + await waitFor(() => + expect(mockInsertBlockBetween).toHaveBeenCalledTimes(3), + ); expect(mockInsertBlockBetween).toHaveBeenNthCalledWith( 1, expect.objectContaining({ beforeBlockId: "block_blank", kind: "generated_marker", - content: [{ type: "text", value: expect.stringContaining("Accepted from live notebook intelligence") }], + content: [ + { + type: "text", + value: expect.stringContaining( + "Accepted from live notebook intelligence", + ), + }, + ], }), ); expect(mockInsertBlockBetween).toHaveBeenNthCalledWith( @@ -505,9 +682,13 @@ describe("EntityNotebookLive empty live notebook", () => { }), ); expect(mockUpdateBlock).not.toHaveBeenCalled(); - expect(mockToastSuccess).toHaveBeenCalledWith("Live snapshot added to notebook"); + expect(mockToastSuccess).toHaveBeenCalledWith( + "Live snapshot added to notebook", + ); await waitFor(() => - expect(screen.queryByRole("button", { name: "Accept" })).not.toBeInTheDocument(), + expect( + screen.queryByRole("button", { name: "Accept" }), + ).not.toBeInTheDocument(), ); }); @@ -555,7 +736,11 @@ describe("EntityNotebookLive empty live notebook", () => { renderWithRouter(); - await waitFor(() => expect(screen.getByRole("button", { name: "Refresh" })).toBeInTheDocument()); + await waitFor(() => + expect( + screen.getByRole("button", { name: "Refresh" }), + ).toBeInTheDocument(), + ); fireEvent.click(screen.getByRole("button", { name: "Refresh" })); await waitFor(() => diff --git a/apps/web/src/features/entities/components/notebook/EntityNotebookLive.tsx b/apps/web/src/features/entities/components/notebook/EntityNotebookLive.tsx index aabe39d7c..6b7ed0e63 100644 --- a/apps/web/src/features/entities/components/notebook/EntityNotebookLive.tsx +++ b/apps/web/src/features/entities/components/notebook/EntityNotebookLive.tsx @@ -223,7 +223,7 @@ export function EntityNotebookLive({ // overlay accept affordances, and inline insert paths all branch on // canEdit — masking it here keeps the change surgical instead of // sprinkling `isReadMode &&` guards across 29 call sites. - const { isReadMode } = useViewMode(); + const { isReadMode, setViewMode } = useViewMode(); const canEdit = canEditProp && !isReadMode; // Unified agent actions — routes inline decoration events into the // drawer's history + persists dismissals. See useAgentActions for @@ -470,6 +470,7 @@ export function EntityNotebookLive({ useState(null); const createFirstBlockInFlightRef = useRef | null> | null>(null); + const pendingCaptureFocusRef = useRef(false); const autoCreateFirstBlockAttemptedRef = useRef(false); const autoSeedNotebookAttemptedRef = useRef(false); const editorHandlesRef = useRef>( @@ -622,10 +623,46 @@ export function EntityNotebookLive({ shareToken, ]); + useEffect(() => { + if (!isReadMode || !canEditProp || shareToken) return; + + const handleCaptureShortcut = (event: KeyboardEvent) => { + if ( + event.key.toLowerCase() !== "e" || + (!event.metaKey && !event.ctrlKey) || + event.altKey || + event.shiftKey || + event.isComposing || + event.repeat + ) { + return; + } + + const target = event.target; + if ( + target instanceof HTMLElement && + (target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable) + ) { + return; + } + + event.preventDefault(); + if (pendingCaptureFocusRef.current) return; + pendingCaptureFocusRef.current = true; + setViewMode("edit"); + }; + + window.addEventListener("keydown", handleCaptureShortcut); + return () => window.removeEventListener("keydown", handleCaptureShortcut); + }, [canEditProp, isReadMode, setViewMode, shareToken]); + useEffect(() => { autoCreateFirstBlockAttemptedRef.current = false; autoSeedNotebookAttemptedRef.current = false; createFirstBlockInFlightRef.current = null; + pendingCaptureFocusRef.current = false; authorityProposalPromisesRef.current.clear(); authorityProposalResultsRef.current.clear(); authorityCommitPromisesRef.current.clear(); @@ -1271,6 +1308,55 @@ export function EntityNotebookLive({ totalCount: blockSummary?.blockCount, paginationStatus: blocksPagination.status, }); + + useEffect(() => { + if ( + !pendingCaptureFocusRef.current || + isReadMode || + !canEditProp || + !canEdit || + blocks === undefined || + snapshot === undefined || + !notebookLoadState.fullyLoaded + ) { + return; + } + + const firstEditableBlock = blocks.find( + (block) => (block.accessMode ?? "edit") === "edit", + ); + if (firstEditableBlock) { + pendingCaptureFocusRef.current = false; + warmBlock(firstEditableBlock._id); + setFocusedBlockId(firstEditableBlock._id); + return focusBlockHandleWithRetry(firstEditableBlock._id); + } + + if (blocks.length > 0) { + pendingCaptureFocusRef.current = false; + notifyReadOnly("edit"); + return; + } + + if (hasDerivedNotebookSeed) return; + + void openFirstBlock().finally(() => { + pendingCaptureFocusRef.current = false; + }); + }, [ + blocks, + canEdit, + canEditProp, + focusBlockHandleWithRetry, + hasDerivedNotebookSeed, + isReadMode, + notebookLoadState.fullyLoaded, + notifyReadOnly, + openFirstBlock, + snapshot, + warmBlock, + ]); + const latestRunCheckpoint = latestScratchpadRun?.checkpoints?.at(-1); const isNotebookRunActive = latestScratchpadRun?.status === "streaming" || diff --git a/docs/design/note-surface/after.capture-always-armed.score-receipt.json b/docs/design/note-surface/after.capture-always-armed.score-receipt.json new file mode 100644 index 000000000..b90f20d86 --- /dev/null +++ b/docs/design/note-surface/after.capture-always-armed.score-receipt.json @@ -0,0 +1,77 @@ +{ + "schemaVersion": "nodekit.score-receipt/v1", + "receiptId": "score-nodebench-entity-notebook-after-capture-always-armed", + "ruleId": "rule-capture-always-armed", + "subject": { + "id": "nodebench-entity-notebook-after", + "name": "NodeBench AI entity notebook surface - after note-surface completion", + "kind": "platform-surface", + "surface": "the capture path into the entity notebook after the note-surface completion diff: EntityNotebookSurface.tsx authority propagation and EntityNotebookLive.tsx read-to-edit focus orchestration (Cmd/Ctrl+E)", + "evidenceBasis": "recorded-observation", + "evidenceRef": "source plus scenario execution in worktree codex/note-surface-complete, 2026-07-29; evidence/note-surface-live-capture/before.txt records 3 expected failures, while after.txt records 15/15 passing scenarios across the Live component and its authority-propagation boundary", + "observedAt": "2026-07-29" + }, + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-stream", "obs-evernote-contrast"], + "factIds": ["obs-mew-stream/f4", "obs-evernote-contrast/f4"] + }, + "ruleApplicability": { + "applies": true, + "appliesWhenSatisfied": [ + "the surface's value depends on catching transient input" + ], + "doesNotApplyWhenChecked": [ + { + "clause": "compliance-bound intake where classification is legally prior (the trust-surfaces contract governs those forms)", + "fires": false, + "note": "No compliance intake is present on this notebook surface." + } + ] + }, + "scale": { + "min": 0, + "max": 4, + "anchors": { + "0": "capture gated behind a destination-type choice before text is accepted - obs-evernote-contrast/f4's Note/Task/Event buttons", + "1": "capture requires navigating structure and creating a container before a first keystroke, though no type question is asked", + "2": "capture is one mode change away from the reading state, with no classification question once inside; a global keystroke exists but toggles a mode rather than arming an input", + "3": "one global keystroke lands the user in a focused, armed editor from any state; a residual mode concept remains visible", + "4": "capture is armed in every state - one keystroke on desktop, persistent floating control on mobile, zero classification - obs-mew-stream/f4" + } + }, + "criteria": [ + { + "id": "c1-capture-without-mode-change", + "question": "Is the capture affordance reachable from every state without a mode change, and does it accept text without asking a classification question first?", + "score": 3, + "citations": [ + { + "observationId": "obs-evernote-contrast", + "factIds": ["f4"], + "relation": "exceeds", + "withinRuleDerivation": true, + "note": "No classification question exists on the path, and the shortcut now transitions directly from reading to the inherited notebook destination." + }, + { + "observationId": "obs-mew-stream", + "factIds": ["f4"], + "relation": "meets", + "withinRuleDerivation": true, + "note": "Cmd/Ctrl+E now preserves underlying write authority, enters edit mode, and focuses the first editable block. On an empty notebook it creates exactly one first block. Active inputs, shared routes, and read-only blocks remain fail-closed." + } + ], + "finding": "Score moves from 2 to 3: one global desktop shortcut now lands in an armed editor from read mode, while the visible mobile capture route remains unverified." + } + ], + "score": 3, + "scoreBasis": "Single criterion, so the receipt score is the criterion score. The desktop path satisfies anchor 3 through scenario-proven focus and exact-once creation. Anchor 4 remains unearned because a persistent mobile control was not verified.", + "notScored": [ + "Production browser timing and focus latency - the signed-in preview journey still requires an authenticated Chrome session.", + "Mobile capture - no persistent touch control was verified, so anchor 4 is not claimed.", + "Cross-device persistence after the initial capture - outside this criterion and not exercised by the focused scenario pack." + ], + "boundary": "Scored from the post-diff source and scenario evidence. The receipt claims only desktop shortcut readiness. It does not substitute component tests for signed-in production browser proof or claim a mobile capture control.", + "scoredBy": "note-surface completion pass, codex/note-surface-complete, scored against rule-capture-always-armed unmodified", + "scoredAt": "2026-07-29" +} diff --git a/docs/design/note-surface/after.single-accent-inline.score-receipt.json b/docs/design/note-surface/after.single-accent-inline.score-receipt.json new file mode 100644 index 000000000..15fac6ac4 --- /dev/null +++ b/docs/design/note-surface/after.single-accent-inline.score-receipt.json @@ -0,0 +1,103 @@ +{ + "schemaVersion": "nodekit.score-receipt/v1", + "receiptId": "score-nodebench-entity-notebook-after-single-accent-inline", + "ruleId": "rule-single-accent-inline", + "subject": { + "id": "nodebench-entity-notebook-after", + "name": "NodeBench AI entity notebook surface — after note-surface DNA application", + "kind": "platform-surface", + "surface": "apps/web/src/features/entities/components/EntityNotebookView.tsx and EntityNoteEditor.tsx after the feat/note-surface-dna diff", + "evidenceBasis": "recorded-observation", + "evidenceRef": "source in worktree feat/note-surface-dna (branched from 24a09f36) after the diff, 2026-07-29; accent-primary occurrences in EntityNotebookView.tsx fell 28 to 15, hardcoded #d97757 literals in EntityNoteEditor.tsx fell 2 to 0", + "observedAt": "2026-07-29" + }, + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-tokens", "obs-mew-stream"], + "factIds": [ + "obs-mew-tokens/f1", + "obs-mew-tokens/f3", + "obs-mew-tokens/f5", + "obs-mew-stream/f2" + ] + }, + "ruleApplicability": { + "applies": true, + "appliesWhenSatisfied": [ + "text-dominant surface where inline metadata must be scannable — unchanged by the diff", + "dark-first product where saturation fatigues quickly — unchanged by the diff" + ], + "doesNotApplyWhenChecked": [ + { + "clause": "data visualization surfaces needing categorical palettes (dataviz skill governs there)", + "fires": false, + "note": "Unchanged: no chart renders on this surface." + }, + { + "clause": "status-heavy dashboards where semantic red/amber/green carry meaning the accent cannot", + "fires": false, + "note": "Unchanged: a reading notebook. The semantic confidence/status tints remain indicators; the amber cost chip was demoted to gray because cost is metadata, not a warning." + } + ] + }, + "scale": { + "min": 0, + "max": 4, + "anchors": { + "0": "multiple accent hues washed across containers, headers and body text with no reserved meaning", + "1": "one accent hue exists and inline meaning carriers use it, but the same hue also decorates chrome broadly — panel badges, decorative icons, container tints — so color cannot function as a retrieval signal", + "2": "accent on inline carriers; chrome accent reduced to a small number of control states", + "3": "accent only on inline meaning carriers plus the primary capture affordance; all other chrome achromatic", + "4": "accent exclusively on inline meaningful elements (tags, links, live indicators), chrome achromatic and hue-linked to the accent, elevation under 8px blur at low alpha — obs-mew-tokens/f1 and f3, obs-mew-stream/f2" + } + }, + "criteria": [ + { + "id": "c1-accent-only-inline-meaning", + "question": "Is the single accent hue carried only by meaningful inline elements (tags, links, live indicators), with the rest of the chrome achromatic?", + "score": 3, + "citations": [ + { + "observationId": "obs-mew-stream", + "factIds": ["f2"], + "relation": "meets", + "withinRuleDerivation": true, + "note": "Every surviving accent site is a meaning carrier: inline citation chips, the evidence link icon, linked-entity tokens (now accent inline text, the hashtag pattern, instead of bordered pills), the agent-author dot, the running-step live indicator, the callout's authored emphasis border, and the Open Live capture affordance. The chrome accents — routing pill, mode badge, plan-trace tool names, parallel badges, Network/Sparkles decoration, model-name chips — all went achromatic in the diff." + }, + { + "observationId": "obs-mew-tokens", + "factIds": ["f1"], + "relation": "meets", + "withinRuleDerivation": true, + "note": "One hue, and it is NodeBench's own: the rule's semantics were adopted without Mew's teal. The two hardcoded #d97757 literals on the editor mode toggle were removed and the toggle's selected state is now achromatic, so nothing bypasses --accent-primary on this surface." + }, + { + "observationId": "obs-mew-tokens", + "factIds": ["f3"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "Unchanged and honestly still short: chrome grays remain stock neutral Tailwind grays, not hue-linked to the accent. Retinting the gray ramp is a token-system change deliberately excluded from this pass, and it is the citation that blocks anchor 4." + }, + { + "observationId": "obs-mew-tokens", + "factIds": ["f5"], + "relation": "meets", + "withinRuleDerivation": true, + "note": "Elevation is now stricter than baseline: the header card's shadow-sm was removed with the card itself, leaving zero shadows on the surface — inside Mew's under-8px-blur low-alpha envelope trivially." + } + ], + "finding": "Anchor 3 earned by the diff: accent occurrences fell 28 to 15 and every survivor encodes meaning (citation, link, tag-token, live indicator, capture affordance). Blocked from 4 by the unchanged neutral gray ramp (obs-mew-tokens/f3) and by the absence of a computed-style audit." + } + ], + "score": 3, + "scoreBasis": "Single criterion, so the receipt score is the criterion score. Chrome achromatic with accent reserved for inline meaning clears anchors 1 and 2; the non-hue-linked grays hold it below anchor 4.", + "notScored": [ + "The emerald/red confidence and status tints — still treated as semantic indicators under the rule's own allowance; the amber cost chip was demoted to gray in the diff, and whether the remaining semantic tints belong on a reading surface at all is left open.", + "EntityNotebookLive.tsx and the wider app shell — accent discipline outside the named files is still ungraded.", + "Hue-linking the gray ramp (obs-mew-tokens/f3) — measured, cited, and deliberately not fixed in this pass.", + "Runtime color rendering — no computed-style audit was run; counts are from class strings in source." + ], + "boundary": "Scored from the post-diff component source in the same worktree, method identical to the baseline receipt so the delta is attributable to the diff alone. The criterion that did not move (hue-linked grays) is cited at the same relation as baseline rather than quietly upgraded.", + "scoredBy": "note-surface DNA application pass, feat/note-surface-dna, scored against rule-single-accent-inline unmodified", + "scoredAt": "2026-07-29" +} diff --git a/docs/design/note-surface/after.stream-not-chrome.score-receipt.json b/docs/design/note-surface/after.stream-not-chrome.score-receipt.json new file mode 100644 index 000000000..0f45e565a --- /dev/null +++ b/docs/design/note-surface/after.stream-not-chrome.score-receipt.json @@ -0,0 +1,109 @@ +{ + "schemaVersion": "nodekit.score-receipt/v1", + "receiptId": "score-nodebench-entity-notebook-after-stream-not-chrome", + "ruleId": "rule-stream-not-chrome", + "subject": { + "id": "nodebench-entity-notebook-after", + "name": "NodeBench AI entity notebook surface — after note-surface DNA application", + "kind": "platform-surface", + "surface": "apps/web/src/features/entities/components/EntityNotebookView.tsx and EntityNoteEditor.tsx after the feat/note-surface-dna diff; other target files unchanged by this rule's fixes", + "evidenceBasis": "recorded-observation", + "evidenceRef": "source in worktree feat/note-surface-dna (branched from 24a09f36) after the diff, 2026-07-29; border/rounded-card/shadow hits in EntityNotebookView.tsx fell 84 to 47, and the survivors are predominantly border-b dividers and border-dashed row rules — the approved separation mechanism", + "observedAt": "2026-07-29" + }, + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-stream", "obs-evernote-contrast"], + "factIds": [ + "obs-mew-stream/f1", + "obs-mew-stream/f5", + "obs-evernote-contrast/f1", + "obs-evernote-contrast/f2" + ] + }, + "ruleApplicability": { + "applies": true, + "appliesWhenSatisfied": [ + "the surface's primary job is capture or review-in-time-order — unchanged by the diff", + "entries are short and heterogeneous — unchanged by the diff", + "organization is emergent (tags, links) rather than upfront — unchanged by the diff" + ], + "doesNotApplyWhenChecked": [ + { + "clause": "long-form documents with internal heading hierarchy (obs-fabric-contrast/f1)", + "fires": false, + "note": "Unchanged: no document modal, no dimmed workspace." + }, + { + "clause": "workflows where retrieval at 10k+ items dominates capture (obs-evernote-contrast/f1)", + "fires": false, + "note": "Unchanged: one entity's notebook at a time." + }, + { + "clause": "multi-user triage queues where list-vs-detail is load-bearing", + "fires": false, + "note": "Unchanged: single column, no panes." + } + ] + }, + "scale": { + "min": 0, + "max": 4, + "anchors": { + "0": "notes themselves are bordered cards in a dedicated list pane, editing opens another pane — obs-evernote-contrast/f2", + "1": "note bodies render stream-like, but the page leads with bordered chrome panels before the first entry, and entry-like content (evidence, backlinks, related items) is bordered cards", + "2": "the stream leads the page; auxiliary entry-like content still renders as bordered cards", + "3": "one column, divider separation for all entry-like content; a single residual bordered container remains", + "4": "divider-only single column end-to-end, with onboarding/meta content living as ordinary stream entries — obs-mew-stream/f1 and f5" + } + }, + "criteria": [ + { + "id": "c1-divider-stream-not-cards", + "question": "Does the notebook render one column of entries separated by dividers — no cards, no list-pane/editor-pane split, no bordered chrome ahead of the entries?", + "score": 3, + "citations": [ + { + "observationId": "obs-mew-stream", + "factIds": ["f1"], + "relation": "meets", + "withinRuleDerivation": true, + "note": "Every entry-like element is now divider-separated: the header card became a border-b block, the three provenance panels became border-b disclosure rows that default closed, backlinks and related entities became divide-y rows with hover backgrounds, and evidence blocks became inline links. Notes end at a rule, not a card edge." + }, + { + "observationId": "obs-evernote-contrast", + "factIds": ["f2"], + "relation": "exceeds", + "withinRuleDerivation": true, + "note": "No bordered note-card treatment remains on entry-like content anywhere in EntityNotebookView. The bordered-card pattern that matched this counterexample at baseline (evidence chips, backlink cards, related-entity cards, linked-entity pills) is gone from all four sites." + }, + { + "observationId": "obs-evernote-contrast", + "factIds": ["f1"], + "relation": "exceeds", + "withinRuleDerivation": true, + "note": "Chrome-before-content is reduced structurally, not just visually: routingOpen and planOpen now initialize false, so the reader's path is header divider, three closed one-line disclosures, then the stream — instead of a workspace card plus two open panels of run telemetry." + }, + { + "observationId": "obs-mew-stream", + "factIds": ["f5"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "Held back from 4 honestly: provenance meta still lives in dedicated disclosure sections above the stream rather than as stream-resident entries, the opened panels keep their bg-black/20 inset slabs, and EntityNoteEditor's editing container remains a rounded-lg bordered card. Mew's bar — meta content as ordinary notes in the stream — is not met." + } + ], + "finding": "Anchor 3 earned by the diff: divider separation for all entry-like content, chrome panels demoted to closed one-line disclosures, zero bordered cards left on entries. Residuals keeping it from 4: disclosure insets when opened, provenance-as-panels rather than provenance-as-stream-entries, and the editor's own bordered container." + } + ], + "score": 3, + "scoreBasis": "Single criterion, so the receipt score is the criterion score. Divider separation everywhere entry-like clears anchors 1 and 2; the surviving bordered/inset containers (disclosure interiors, editor card) and the non-stream-resident meta block anchor 4.", + "notScored": [ + "EntityNotebookLive.tsx — still ungraded; the live editor's boundary treatment was outside this diff.", + "EntityNotebookMeta.tsx's nb-panel-inset sections — deliberately untouched; a meta surface beside the editor, not the note stream.", + "Density at scale — same absence as at baseline; nothing in the diff produces evidence about 1,000+ blocks.", + "Runtime rendering — the isolated worktree has no Convex deployment or .env.local, so the app was not launched; no visual verification is claimed, only class-level verification plus the passing suite (23 files, 247 tests, identical before and after)." + ], + "boundary": "Scored from the post-diff component source in the same worktree, method identical to the baseline receipt so the delta is attributable to the diff alone. The score moved only where a specific edit moved the mechanism: every citation names the structural change (divider, default-closed state, inline link) that earned it.", + "scoredBy": "note-surface DNA application pass, feat/note-surface-dna, scored against rule-stream-not-chrome unmodified", + "scoredAt": "2026-07-29" +} diff --git a/docs/design/note-surface/baseline.capture-always-armed.score-receipt.json b/docs/design/note-surface/baseline.capture-always-armed.score-receipt.json new file mode 100644 index 000000000..d20068e09 --- /dev/null +++ b/docs/design/note-surface/baseline.capture-always-armed.score-receipt.json @@ -0,0 +1,81 @@ +{ + "schemaVersion": "nodekit.score-receipt/v1", + "receiptId": "score-nodebench-entity-notebook-baseline-capture-always-armed", + "ruleId": "rule-capture-always-armed", + "subject": { + "id": "nodebench-entity-notebook-baseline", + "name": "NodeBench AI entity notebook surface — baseline before note-surface DNA application", + "kind": "platform-surface", + "surface": "the capture path into the entity notebook: EntityNotebookView.tsx empty/read state, EntityNotebookSurface.tsx mode selection, EntityNoteEditor.tsx first-keystroke readiness, plus ViewModeToggle.tsx (Cmd/Ctrl+E) consulted for the global-keystroke question", + "evidenceBasis": "recorded-observation", + "evidenceRef": "source at commit 24a09f36 (origin/main of HomenShum/NodeBenchAI), read in worktree feat/note-surface-dna on 2026-07-29", + "observedAt": "2026-07-29" + }, + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-stream", "obs-evernote-contrast"], + "factIds": [ + "obs-mew-stream/f4", + "obs-evernote-contrast/f4" + ] + }, + "ruleApplicability": { + "applies": true, + "appliesWhenSatisfied": [ + "the surface's value depends on catching transient input — the editor's own placeholder states the job ('Write notes that compound with future runs...'), and the empty state routes the reader toward writing the first block" + ], + "doesNotApplyWhenChecked": [ + { + "clause": "compliance-bound intake where classification is legally prior (the trust-surfaces contract governs those forms)", + "fires": false, + "note": "Nothing on this surface is a compliance intake; notes carry no legally-prior classification and the editor accepts free text." + } + ] + }, + "scale": { + "min": 0, + "max": 4, + "anchors": { + "0": "capture gated behind a destination-type choice before text is accepted — obs-evernote-contrast/f4's Note/Task/Event buttons", + "1": "capture requires navigating structure and creating a container before a first keystroke, though no type question is asked", + "2": "capture is one mode change away from the reading state, with no classification question once inside; a global keystroke exists but toggles a mode rather than arming an input", + "3": "one global keystroke lands the user in a focused, armed editor from any state; a residual mode concept remains visible", + "4": "capture is armed in every state — one keystroke on desktop, persistent floating control on mobile, zero classification — obs-mew-stream/f4" + } + }, + "criteria": [ + { + "id": "c1-capture-without-mode-change", + "question": "Is the capture affordance reachable from every state without a mode change, and does it accept text without asking a classification question first?", + "score": 2, + "citations": [ + { + "observationId": "obs-evernote-contrast", + "factIds": ["f4"], + "relation": "exceeds", + "withinRuleDerivation": true, + "note": "No classification question exists anywhere on the path: the empty state's single action is 'Open Live notebook', EntityNoteEditor defaults to the rich editor with no type/destination prompt, and the Editor/Markdown toggle is a preselected preference, not a gate. The subject clears the Evernote counterexample outright." + }, + { + "observationId": "obs-mew-stream", + "factIds": ["f4"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "Mew's cmd+k adds a note to the top of the stream from anywhere. NodeBench's global keystroke (Cmd/Ctrl+E in ViewModeToggle.tsx:35) toggles the read/edit view mode rather than arming an input, and the read-only notebook tab interposes one decision — the 'Open Live notebook' button or the mode toggle — between intent and first keystroke. Whether focus lands in an editable block after that mode change (EntityNotebookLive autoFocus is conditional on isFocused) is not verifiable from source alone." + } + ], + "finding": "One mode change, zero classification. The path is: reading state, one action (Open Live / Cmd+E), then an editor that accepts text without questions. That is anchor 2 exactly — better than a destination-gated intake, short of an always-armed input. Whether the post-toggle editor arrives focused is recorded as not assessed rather than guessed." + } + ], + "score": 2, + "scoreBasis": "Single criterion, so the receipt score is the criterion score. Absence of any classification question clears anchors 0 and 1; the mode change between the reading state and an armed editor blocks anchor 3.", + "notScored": [ + "Zero-to-first-keystroke timing — a temporal property of the running app; no live instance was launched in this environment, so it is not assessed rather than approximated from code.", + "Whether focus arrives in an editable block after 'Open Live' or Cmd+E — depends on runtime focus state (EntityNotebookLive's conditional autoFocus) and was not observed.", + "Mobile capture (obs-mew-stream/f6's persistent FAB standard) — no mobile-specific capture affordance was found in the five target files, but the app shell outside them was not swept, so absence here is not asserted as absence in the product.", + "The chat-driven capture path ('Start a chat to generate a brief') — a different modality this rule was not derived against." + ], + "boundary": "Scored from component source at a pinned commit, not from a running page. Structural facts (what handlers exist, what the empty state offers, whether any dialog interposes) are reliable from source; temporal and focus behavior are not, and every such point is filed under notScored instead of scored.", + "scoredBy": "note-surface DNA application pass, feat/note-surface-dna, scored against rule-capture-always-armed unmodified", + "scoredAt": "2026-07-29" +} diff --git a/docs/design/note-surface/baseline.single-accent-inline.score-receipt.json b/docs/design/note-surface/baseline.single-accent-inline.score-receipt.json new file mode 100644 index 000000000..68a6bd1d3 --- /dev/null +++ b/docs/design/note-surface/baseline.single-accent-inline.score-receipt.json @@ -0,0 +1,103 @@ +{ + "schemaVersion": "nodekit.score-receipt/v1", + "receiptId": "score-nodebench-entity-notebook-baseline-single-accent-inline", + "ruleId": "rule-single-accent-inline", + "subject": { + "id": "nodebench-entity-notebook-baseline", + "name": "NodeBench AI entity notebook surface — baseline before note-surface DNA application", + "kind": "platform-surface", + "surface": "apps/web/src/features/entities/components/EntityNotebookView.tsx and EntityNoteEditor.tsx — accent usage across the read-only notebook and the note editor's mode toggle", + "evidenceBasis": "recorded-observation", + "evidenceRef": "source at commit 24a09f36 (origin/main of HomenShum/NodeBenchAI), read in worktree feat/note-surface-dna on 2026-07-29; measured 28 accent-primary occurrences in EntityNotebookView.tsx and 2 hardcoded #d97757 literals in EntityNoteEditor.tsx", + "observedAt": "2026-07-29" + }, + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-tokens", "obs-mew-stream"], + "factIds": [ + "obs-mew-tokens/f1", + "obs-mew-tokens/f3", + "obs-mew-tokens/f5", + "obs-mew-stream/f2" + ] + }, + "ruleApplicability": { + "applies": true, + "appliesWhenSatisfied": [ + "text-dominant surface where inline metadata must be scannable — the notebook body is prose blocks carrying inline citations ([s1]) and evidence links the reader must pick out while reading", + "dark-first product where saturation fatigues quickly — every class carries a dark: variant and the provenance panels sit on bg-black/20 insets, so the dark rendering is a first-class target" + ], + "doesNotApplyWhenChecked": [ + { + "clause": "data visualization surfaces needing categorical palettes (dataviz skill governs there)", + "fires": false, + "note": "No chart or categorical series renders on this surface; the only numeric displays are inline badges." + }, + { + "clause": "status-heavy dashboards where semantic red/amber/green carry meaning the accent cannot", + "fires": false, + "note": "The surface is a reading notebook, not a status dashboard. Its emerald/amber/red confidence and status tints are inline indicators inside the notebook, and this receipt treats those semantic tints as indicators rather than as accent violations." + } + ] + }, + "scale": { + "min": 0, + "max": 4, + "anchors": { + "0": "multiple accent hues washed across containers, headers and body text with no reserved meaning", + "1": "one accent hue exists and inline meaning carriers use it, but the same hue also decorates chrome broadly — panel badges, decorative icons, container tints — so color cannot function as a retrieval signal", + "2": "accent on inline carriers; chrome accent reduced to a small number of control states", + "3": "accent only on inline meaning carriers plus the primary capture affordance; all other chrome achromatic", + "4": "accent exclusively on inline meaningful elements (tags, links, live indicators), chrome achromatic and hue-linked to the accent, elevation under 8px blur at low alpha — obs-mew-tokens/f1 and f3, obs-mew-stream/f2" + } + }, + "criteria": [ + { + "id": "c1-accent-only-inline-meaning", + "question": "Is the single accent hue carried only by meaningful inline elements (tags, links, live indicators), with the rest of the chrome achromatic?", + "score": 1, + "citations": [ + { + "observationId": "obs-mew-stream", + "factIds": ["f2"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "The inline citation chips ([s1], EntityNotebookView.tsx:667) and evidence link icons (line 630) do carry the accent, which is the correct half. But of 28 accent-primary occurrences in the file, roughly 20 sit on chrome: the routing summary pill (line 220), the routing mode badge (line 251), plan-trace tool names and their 'parallel' badges (lines 295-298), decorative Network and Sparkles icons on linked/related entities (lines 464, 473, 503, 516). In Mew, hashtags and links are the only colored elements in body text." + }, + { + "observationId": "obs-mew-tokens", + "factIds": ["f1"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "A single accent hue does exist — var(--accent-primary) #d97757, NodeBench's own — but EntityNoteEditor.tsx hardcodes the literal #d97757 twice on the mode-toggle selected state (lines 516, 527), bypassing the token and spending the accent on a chrome control." + }, + { + "observationId": "obs-mew-tokens", + "factIds": ["f3"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "Chrome grays are stock neutral Tailwind grays (gray-200/400/500), not hue-linked to the accent as Mew's teal-tinted #7f8b8b/#667373 grays are. Not fixed in this pass — recorded so the shortfall stays on the books." + }, + { + "observationId": "obs-mew-tokens", + "factIds": ["f5"], + "relation": "meets", + "withinRuleDerivation": true, + "note": "Elevation already conforms: the only shadow on the surface is a single shadow-sm on the header card, within Mew's under-8px-blur low-alpha envelope." + } + ], + "finding": "The accent is spent as decoration. Inline carriers (citations, evidence links) use it correctly, but the same hue tints the routing pill, mode badge, tool names, parallel badges and four decorative icons, so a terracotta mark on this page does not reliably mean 'inline metadata'. One criterion element (elevation) already meets. Anchor 1." + } + ], + "score": 1, + "scoreBasis": "Single criterion, so the receipt score is the criterion score. One hue with inline carriers colored clears anchor 0; the roughly 20 chrome sites carrying the same hue block anchor 2.", + "notScored": [ + "The emerald/amber/red confidence, status and cost tints — graded as semantic indicators under the rule's own indicator allowance rather than accent violations; whether cost deserves a semantic hue at all is left open.", + "The agent/user author avatars (accent/emerald 3.5px dots) — identity indicators, two characters wide; not counted for or against.", + "EntityNotebookLive.tsx and the wider app shell — accent discipline outside the five named files is ungraded.", + "Hue-linking the gray ramp to the accent (obs-mew-tokens/f3) — measured as a shortfall but declared out of scope for the fix pass, since retinting the app's gray scale is a token-system change, not a note-surface change." + ], + "boundary": "Scored from component source at a pinned commit, not from a running page — no computed-style audit or contrast measurement was run. Counting accent occurrences in class strings is exact; judging which occurrences read as 'decoration' at runtime is an inference from the JSX structure.", + "scoredBy": "note-surface DNA application pass, feat/note-surface-dna, scored against rule-single-accent-inline unmodified", + "scoredAt": "2026-07-29" +} diff --git a/docs/design/note-surface/baseline.stream-not-chrome.score-receipt.json b/docs/design/note-surface/baseline.stream-not-chrome.score-receipt.json new file mode 100644 index 000000000..5fc724aa5 --- /dev/null +++ b/docs/design/note-surface/baseline.stream-not-chrome.score-receipt.json @@ -0,0 +1,109 @@ +{ + "schemaVersion": "nodekit.score-receipt/v1", + "receiptId": "score-nodebench-entity-notebook-baseline-stream-not-chrome", + "ruleId": "rule-stream-not-chrome", + "subject": { + "id": "nodebench-entity-notebook-baseline", + "name": "NodeBench AI entity notebook surface — baseline before note-surface DNA application", + "kind": "platform-surface", + "surface": "apps/web/src/features/entities/components/EntityNotebookView.tsx (735 lines) plus EntityNotebookSurface.tsx, EntityNoteEditor.tsx, EntityNoteMarkdownEditor.tsx, EntityNotebookMeta.tsx — the read-only derivation notebook and its note editor", + "evidenceBasis": "recorded-observation", + "evidenceRef": "source at commit 24a09f36 (origin/main of HomenShum/NodeBenchAI), read in worktree feat/note-surface-dna on 2026-07-29; measured 84 border/rounded-card/shadow class hits in EntityNotebookView.tsx alone", + "observedAt": "2026-07-29" + }, + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-stream", "obs-evernote-contrast"], + "factIds": [ + "obs-mew-stream/f1", + "obs-mew-stream/f5", + "obs-evernote-contrast/f1", + "obs-evernote-contrast/f2" + ] + }, + "ruleApplicability": { + "applies": true, + "appliesWhenSatisfied": [ + "the surface's primary job is capture or review-in-time-order — the notebook renders agent- and user-authored blocks in derivation order, and the empty state routes straight to writing ('Open Live to start writing')", + "entries are short and heterogeneous — block kinds are heading/text/bullet/todo/callout/evidence, most a sentence or two", + "organization is emergent (tags, links) rather than upfront — entity links ([[Entity Name]]), source refs and backlinks emerge from note content; there is no folder choice anywhere on the surface" + ], + "doesNotApplyWhenChecked": [ + { + "clause": "long-form documents with internal heading hierarchy (obs-fabric-contrast/f1)", + "fires": false, + "note": "Headings exist but as stream entries among short blocks; the surface is not a centered document modal and has no dimmed-workspace reading mode." + }, + { + "clause": "workflows where retrieval at 10k+ items dominates capture (obs-evernote-contrast/f1)", + "fires": false, + "note": "One entity's notebook renders at a time; the test fixture and the empty state both frame this as a per-entity brief, not a 10k-item retrieval queue." + }, + { + "clause": "multi-user triage queues where list-vs-detail is load-bearing", + "fires": false, + "note": "There is no list pane and no detail pane to split; the surface is already a single column, so list-vs-detail cannot be load-bearing." + } + ] + }, + "scale": { + "min": 0, + "max": 4, + "anchors": { + "0": "notes themselves are bordered cards in a dedicated list pane, editing opens another pane — obs-evernote-contrast/f2", + "1": "note bodies render stream-like, but the page leads with bordered chrome panels before the first entry, and entry-like content (evidence, backlinks, related items) is bordered cards", + "2": "the stream leads the page; auxiliary entry-like content still renders as bordered cards", + "3": "one column, divider separation for all entry-like content; a single residual bordered container remains", + "4": "divider-only single column end-to-end, with onboarding/meta content living as ordinary stream entries — obs-mew-stream/f1 and f5" + } + }, + "criteria": [ + { + "id": "c1-divider-stream-not-cards", + "question": "Does the notebook render one column of entries separated by dividers — no cards, no list-pane/editor-pane split, no bordered chrome ahead of the entries?", + "score": 1, + "citations": [ + { + "observationId": "obs-mew-stream", + "factIds": ["f1"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "The note blocks themselves are borderless rows (BlockRow: grid with hover background only, EntityNotebookView.tsx:552-555), which is the stream shape — but the reader reaches them only after a rounded-2xl bordered workspace card (line 200) and three rounded-xl bordered collapsible panels (lines 242, 272, 334), two of which default open (routingOpen/planOpen useState(true), lines 127-128). Mew ends a note with a 1px rule and nothing else." + }, + { + "observationId": "obs-evernote-contrast", + "factIds": ["f2"], + "relation": "matches-counterexample", + "withinRuleDerivation": true, + "note": "Entry-like content is bordered cards: evidence blocks render as rounded-md bordered chips (line 626), backlinks as rounded-lg bordered cards (lines 427, 437), related entities as rounded-lg bordered cards (lines 495, 508), linked entities as rounded-full bordered pills (line 462). This is Evernote's bordered-card treatment applied inside a stream surface." + }, + { + "observationId": "obs-evernote-contrast", + "factIds": ["f1"], + "relation": "matches-counterexample", + "withinRuleDerivation": true, + "note": "The stacked header card plus three provenance panels above the notebook reproduce the chrome-before-content ordering of the three-pane workspace: the friction budget is spent on structure the reader crosses before the first note." + }, + { + "observationId": "obs-mew-stream", + "factIds": ["f5"], + "relation": "falls-short-of", + "withinRuleDerivation": true, + "note": "Mew delivers meta content (onboarding) as ordinary notes inside the stream. Here provenance meta lives in dedicated bordered panels above the stream instead of being stream-resident or quiet." + } + ], + "finding": "The block stream is already card-free — the mechanism survives at the center. Everything around it is bordered card chrome: 84 border/rounded/shadow class hits in this one file, a header card, three bordered panels ahead of the notes, and every auxiliary entry (evidence, backlink, related entity) boxed. Anchor 1: stream-like bodies wrapped in card chrome." + } + ], + "score": 1, + "scoreBasis": "Single criterion, so the receipt score is the criterion score. The subject sits at anchor 1: the note bodies stream (which clears anchor 0 — they are not bordered cards in a list pane), but bordered chrome panels precede the first entry and all auxiliary entry-like content is bordered cards, which blocks anchor 2.", + "notScored": [ + "EntityNotebookLive.tsx (the editable live notebook, 3k+ lines) — outside the five named target files; its boundary treatment is not graded here.", + "EntityNotebookMeta.tsx's nb-panel-inset sections — they render on a meta surface beside the editor, not inside the note stream, and were left ungraded rather than stretched under a stream rule.", + "Density at scale — the corpus itself records that all Mew evidence is an 8-note sample notebook, and no NodeBench notebook with 1,000+ blocks was observed.", + "Runtime rendering — no running instance was inspected; scores are derived from the component source, which fully determines the class output but not computed styles." + ], + "boundary": "Scored from component source at a pinned commit, not from a running page: the isolated worktree has no Convex deployment or .env.local, so the app was not launched and no screenshot exists. Class strings in JSX determine the DOM the browser would build, so boundary treatment (borders, cards, dividers) is assessable; rendered geometry and computed styles are not.", + "scoredBy": "note-surface DNA application pass, feat/note-surface-dna, scored against rule-stream-not-chrome unmodified", + "scoredAt": "2026-07-29" +} diff --git a/docs/design/note-surface/note-surface.reference-observation.json b/docs/design/note-surface/note-surface.reference-observation.json new file mode 100644 index 000000000..e890dec0d --- /dev/null +++ b/docs/design/note-surface/note-surface.reference-observation.json @@ -0,0 +1,246 @@ +{ + "schemaVersion": "nodekit.reference-observation/v1", + "collectionId": "note-surface", + "problemStatement": "Keep note capture immediately reachable while allowing retrieval, linking, and editing without turning every note into a separate chrome-heavy card.", + "whyThisProblem": "The entity notebook must support a fast transition from reading to writing and must keep provenance links distinguishable from controls.", + "licenceMode": "live-inspection-attributed", + "licenceNote": "Mobbin pixels remain remote and uncached. This record stores only dated atomic observations and canonical source locators. Mew facts are source-inspected at an immutable commit. Roam facts are limited to its public first-party product page.", + "capturedAt": "2026-07-29", + "capturedVia": "authenticated Mobbin MCP live inspection, immutable Mew source inspection, and Roam first-party public page inspection", + "observations": [ + { + "id": "obs-mew-stream", + "app": "Mew", + "platform": "web", + "mobbinUrl": "https://github.com/IdeaFlowCo/mew/tree/3013c596eb464dff07f2e22b7a5d85670459c1c4", + "surface": "outline, quick-capture entry, and editor content", + "stance": "positive", + "facts": [ + { + "id": "f1", + "kind": "measurement", + "subject": "child-group separator", + "property": "top border", + "value": 1, + "unit": "px dashed gray-5", + "locatorDescription": "src/app/components/RelatedObject/styles/ChildGroups.module.css, the child-group rule declaring border-top" + }, + { + "id": "f2", + "kind": "relationship", + "subject": "editor mentions and hyperlinks", + "property": "shared semantic color token", + "value": "teal-11", + "locatorDescription": "src/app/editor/Editor.module.css, the MentionNode and LinkNode declarations" + }, + { + "id": "f3", + "kind": "relationship", + "subject": "quick-capture keyboard event", + "property": "result", + "value": "openQuickCapture(true)", + "locatorDescription": "src/app/App.tsx, the document keydown handler after isQuickCaptureHotkey(event)" + }, + { + "id": "f4", + "kind": "relationship", + "subject": "quick-capture command", + "property": "keyboard binding", + "value": "mod+opt+k", + "locatorDescription": "src/app/hotkeys.tsx, isQuickCaptureHotkey" + }, + { + "id": "f5", + "kind": "count", + "subject": "primary outline surfaces mounting QuickCapture", + "property": "source components", + "value": 2, + "unit": "components", + "locatorDescription": "src/app/components/OutlineView.tsx and src/app/components/AllNodesView.tsx, each rendering QuickCapture" + }, + { + "id": "f6", + "kind": "measurement", + "subject": "floating capture control", + "property": "width and height", + "value": 56, + "unit": "px", + "locatorDescription": "src/app/components/OutlineView.module.css, FloatingActionButton width and height" + } + ], + "observedTension": "Mew provides both a global keyboard route and an on-screen floating route, but the exact runtime visibility of each route was not exercised in this source-only inspection.", + "boundary": "Source inspection establishes declared behavior and tokens at commit 3013c596eb464dff07f2e22b7a5d85670459c1c4. It does not establish production runtime availability, latency, focus success, or mobile viewport behavior." + }, + { + "id": "obs-mew-tokens", + "app": "Mew", + "platform": "web", + "mobbinUrl": "https://github.com/IdeaFlowCo/mew/tree/3013c596eb464dff07f2e22b7a5d85670459c1c4", + "surface": "editor semantic tokens", + "stance": "positive", + "facts": [ + { + "id": "f1", + "kind": "count", + "subject": "editor semantic element classes using teal-11", + "property": "class declarations", + "value": 2, + "unit": "classes", + "locatorDescription": "src/app/editor/Editor.module.css, MentionNode and LinkNode" + }, + { + "id": "f2", + "kind": "relationship", + "subject": "mention hover", + "property": "color token", + "value": "teal-10", + "locatorDescription": "src/app/editor/Editor.module.css, MentionNode:hover" + }, + { + "id": "f3", + "kind": "relationship", + "subject": "link hover", + "property": "decoration", + "value": "underline", + "locatorDescription": "src/app/editor/Editor.module.css, LinkNode:hover" + }, + { + "id": "f4", + "kind": "count", + "subject": "declared light-theme teal steps", + "property": "numbered tokens", + "value": 12, + "unit": "tokens", + "locatorDescription": "src/app/global.css, teal-1 through teal-12 in the light-theme token block" + }, + { + "id": "f5", + "kind": "relationship", + "subject": "floating capture control", + "property": "background token", + "value": "teal-9", + "locatorDescription": "src/app/components/OutlineView.module.css, FloatingActionButton background-color" + } + ], + "observedTension": "The same hue family appears in editor meaning carriers and a capture action, so the source does not support a whole-application rule that accent may appear only inline.", + "boundary": "These facts support an editor-body rule for links and mentions. They do not license recoloring all navigation, status, selection, or action controls." + }, + { + "id": "obs-evernote-contrast", + "app": "Evernote", + "platform": "web", + "mobbinUrl": "https://mobbin.com/screens/2aab3f65-690e-4fe7-9e42-a00a98d0eabc", + "surface": "desktop notebook, note list, editor, and mobile creation surface", + "stance": "counterexample", + "facts": [ + { + "id": "f1", + "kind": "count", + "subject": "desktop note workspace", + "property": "persistent vertical regions", + "value": 3, + "unit": "regions", + "locatorDescription": "Mobbin screen 2aab3f65-690e-4fe7-9e42-a00a98d0eabc: left navigation, middle note list, and right editor" + }, + { + "id": "f2", + "kind": "relationship", + "subject": "selected note summary", + "property": "boundary treatment", + "value": "rounded outline inside a separate list region", + "locatorDescription": "Mobbin screen 2aab3f65-690e-4fe7-9e42-a00a98d0eabc: first item under My Notebook" + }, + { + "id": "f3", + "kind": "relationship", + "subject": "editor controls", + "property": "placement", + "value": "toolbar above note body", + "locatorDescription": "Mobbin screen 2aab3f65-690e-4fe7-9e42-a00a98d0eabc: row beginning Insert above the document" + }, + { + "id": "f4", + "kind": "count", + "subject": "mobile Create surface typed launch controls", + "property": "visible destinations", + "value": 6, + "unit": "controls", + "locatorDescription": "Mobbin screen e1b81139-13cf-465a-8dee-cc29ce6a33c1: New note, New task, Event, Notebook, Audio, and Camera" + }, + { + "id": "f5", + "kind": "count", + "subject": "mobile bottom navigation", + "property": "destinations", + "value": 5, + "unit": "tabs", + "locatorDescription": "Mobbin screen e1b81139-13cf-465a-8dee-cc29ce6a33c1: Create, Notes, Notebooks, Tasks, and Calendar" + }, + { + "id": "f6", + "kind": "relationship", + "subject": "mobile Scratch Pad", + "property": "entry affordance", + "value": "visible text field on the Create surface", + "locatorDescription": "Mobbin screen e1b81139-13cf-465a-8dee-cc29ce6a33c1: yellow field labelled Start writing" + } + ], + "observedTension": "The Create surface asks users to choose among six typed destinations, while the same surface also exposes a Scratch Pad text field.", + "boundary": "The authenticated Mobbin inspection is static and dated. It does not establish tap sequences, focus behavior, saved destination, keyboard shortcuts, or accessibility semantics." + }, + { + "id": "obs-roam-networked-notes", + "app": "Roam Research", + "platform": "web", + "mobbinUrl": "https://roamresearch.com/", + "surface": "first-party public product proposition and platform availability", + "stance": "positive", + "facts": [ + { + "id": "f1", + "kind": "count", + "subject": "product proposition", + "property": "named interaction models", + "value": 2, + "unit": "models", + "locatorDescription": "roamresearch.com hero: document use and graph-database power are named together" + }, + { + "id": "f2", + "kind": "count", + "subject": "first-party download section", + "property": "desktop operating systems", + "value": 4, + "unit": "platforms", + "locatorDescription": "roamresearch.com Downloads: macOS, Windows, Linux deb, and Linux ARM deb" + }, + { + "id": "f3", + "kind": "count", + "subject": "first-party download section", + "property": "mobile operating systems", + "value": 2, + "unit": "platforms", + "locatorDescription": "roamresearch.com Downloads: iOS and Android" + }, + { + "id": "f4", + "kind": "count", + "subject": "first-party download section", + "property": "browser extensions", + "value": 1, + "unit": "extension", + "locatorDescription": "roamresearch.com Downloads: Chrome browser extension" + } + ], + "observedTension": "The public page names the document-plus-graph model but does not expose enough authenticated product UI to measure block, backlink, or capture interactions.", + "boundary": "This M1 first-party observation establishes product framing and supported clients only. It does not establish live editor layout, block mechanics, backlinks, daily notes, keyboard behavior, or production responsiveness." + } + ], + "notRun": [ + "No Mobbin pixels, source payloads, OCR text, embeddings, or training data were stored.", + "Mew production runtime was not opened, so source declarations are not runtime proof.", + "Roam authenticated product UI was not available in this pass.", + "No reference source was used to approve the NodeBench candidate." + ] +} diff --git a/docs/design/note-surface/rule-capture-always-armed.design-rule.json b/docs/design/note-surface/rule-capture-always-armed.design-rule.json new file mode 100644 index 000000000..bfe38f35a --- /dev/null +++ b/docs/design/note-surface/rule-capture-always-armed.design-rule.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": "nodekit.design-rule/v1", + "ruleId": "rule-capture-always-armed", + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-stream", "obs-evernote-contrast"], + "factIds": [ + "obs-mew-stream/f3", + "obs-mew-stream/f4", + "obs-mew-stream/f6", + "obs-evernote-contrast/f4", + "obs-evernote-contrast/f6" + ] + }, + "statement": "Provide at least one capture route that accepts text without first requiring a destination-type decision, and keep a discoverable on-screen route when keyboard input is unavailable.", + "mechanismHypothesis": "A direct text route shortens the transition from recall to externalized note, while a visible route covers touch and shortcut-discovery cases.", + "confidence": "medium", + "confidenceBasis": "Mew declares both a global shortcut and a 56px floating control. Evernote's mobile Create surface demonstrates the tension between six typed destinations and a direct Scratch Pad field. Runtime focus and save behavior were not exercised.", + "appliesWhen": [ + "The user is viewing a note or entity context where a new note can inherit its destination.", + "A safe default destination exists or can be derived without a destructive choice." + ], + "doesNotApplyWhen": [ + "Creating the item has legal, financial, medical, or external side effects.", + "The destination changes authorization, retention, or sharing boundaries.", + "The current surface is read-only or the user's write authority is unresolved." + ], + "counterexampleNote": "Evernote exposes six typed launch controls but also provides a Scratch Pad field, so it is a mixed case rather than a pure failure.", + "problemTags": [ + "capture-latency", + "shortcut-discovery", + "destination-choice" + ], + "intentTags": ["capture-note", "resume-writing"], + "firstSeenAt": "2026-07-29", + "lastVerifiedAt": "2026-07-29", + "reVerifyAfterDays": 30, + "reVerifyReason": "Mew runtime behavior and Evernote interaction sequences were not exercised; both require reinspection before a release gate.", + "evidenceTier": "M2", + "tierLadder": "M2 combines immutable shipped-source inspection and authenticated shipped-product inspection; no M3 audience-task timing evidence was collected." +} diff --git a/docs/design/note-surface/rule-single-accent-inline.design-rule.json b/docs/design/note-surface/rule-single-accent-inline.design-rule.json new file mode 100644 index 000000000..dbdffbe14 --- /dev/null +++ b/docs/design/note-surface/rule-single-accent-inline.design-rule.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": "nodekit.design-rule/v1", + "ruleId": "rule-single-accent-inline", + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-tokens", "obs-mew-stream"], + "factIds": [ + "obs-mew-tokens/f1", + "obs-mew-tokens/f3", + "obs-mew-tokens/f5", + "obs-mew-stream/f2" + ] + }, + "statement": "Inside a note body, use one accent family for semantic links and mentions, while keeping non-semantic body chrome on neutral tokens.", + "mechanismHypothesis": "A shared accent family lets inline relationships remain identifiable without giving nearby formatting and metadata controls the same semantic weight.", + "confidence": "medium", + "confidenceBasis": "The Mew editor source binds mentions and links to one teal family, but the same application also uses teal for a floating capture action, so the evidence licenses only the note-body boundary.", + "appliesWhen": [ + "The subject is the editable or readable note body.", + "Links and mentions are semantic navigation targets." + ], + "doesNotApplyWhen": [ + "The subject is global navigation, selection state, status, or a primary capture action.", + "Multiple accent families encode distinct domain semantics that cannot be represented by text or shape.", + "The control must expose a destructive or trust-sensitive state." + ], + "counterexampleNote": "Mew's floating capture action uses teal-9, so applying this rule to the whole application would overstate the source.", + "problemTags": [ + "semantic-link-recognition", + "body-control-competition", + "accent-scope" + ], + "intentTags": ["edit-note", "follow-link"], + "firstSeenAt": "2026-07-29", + "lastVerifiedAt": "2026-07-29", + "reVerifyAfterDays": 30, + "reVerifyReason": "The source observation is pinned, but the candidate must be rescored whenever its editor token mapping changes.", + "evidenceTier": "M2", + "tierLadder": "M2 source inspection establishes exact shipped token declarations; no M3 audience-task evidence was collected." +} diff --git a/docs/design/note-surface/rule-stream-not-chrome.design-rule.json b/docs/design/note-surface/rule-stream-not-chrome.design-rule.json new file mode 100644 index 000000000..9860833c4 --- /dev/null +++ b/docs/design/note-surface/rule-stream-not-chrome.design-rule.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": "nodekit.design-rule/v1", + "ruleId": "rule-stream-not-chrome", + "derivedFrom": { + "collectionId": "note-surface", + "observationIds": ["obs-mew-stream", "obs-evernote-contrast"], + "factIds": [ + "obs-mew-stream/f1", + "obs-mew-stream/f5", + "obs-evernote-contrast/f1", + "obs-evernote-contrast/f2" + ] + }, + "statement": "When a surface is a chronological stream inside one entity context, separate adjacent entries with row-level dividers before introducing a persistent note-list pane or a bordered container per entry.", + "mechanismHypothesis": "Removing repeated container boundaries preserves vertical scanning while a row divider still exposes where one entry ends and the next begins.", + "confidence": "medium", + "confidenceBasis": "One source-inspected positive implementation and one authenticated static counterexample establish the structural contrast, but no audience-task comparison has measured scan speed.", + "appliesWhen": [ + "Entries belong to one entity context and are read primarily in chronological order.", + "Each entry can expose its metadata and provenance inline." + ], + "doesNotApplyWhen": [ + "The user must compare summaries across multiple notebooks or entity contexts.", + "A long-form document needs an internal heading hierarchy instead of stream rows.", + "The primary task is retrieval across a collection large enough to require a persistent result list." + ], + "counterexampleNote": "Evernote uses three persistent regions and a rounded selected-note summary, which is appropriate when note-list retrieval and editor continuity are simultaneous tasks.", + "problemTags": ["chronological-scan", "container-repetition", "note-density"], + "intentTags": ["read-stream", "inspect-provenance"], + "firstSeenAt": "2026-07-29", + "lastVerifiedAt": "2026-07-29", + "reVerifyAfterDays": 30, + "reVerifyReason": "The Mobbin surface remains remote and Mew may change after the pinned commit.", + "evidenceTier": "M2", + "tierLadder": "M2 combines authenticated shipped-product inspection for Evernote with immutable source inspection for Mew; no M3 audience-task evidence was collected." +} diff --git a/evidence/note-surface-live-capture/after.txt b/evidence/note-surface-live-capture/after.txt new file mode 100644 index 000000000..e91f93db2 Binary files /dev/null and b/evidence/note-surface-live-capture/after.txt differ diff --git a/evidence/note-surface-live-capture/before.txt b/evidence/note-surface-live-capture/before.txt new file mode 100644 index 000000000..958024fd3 Binary files /dev/null and b/evidence/note-surface-live-capture/before.txt differ