diff --git a/docs/adr/0073-llm-structure-adjudication.md b/docs/adr/0073-llm-structure-adjudication.md index 4055573df..a6d964ac1 100644 --- a/docs/adr/0073-llm-structure-adjudication.md +++ b/docs/adr/0073-llm-structure-adjudication.md @@ -32,6 +32,11 @@ override a resolved unit. All LLM and vision requests continue through contextual-orchestrator. The caller does not select an LLM model. +The buyer renderer treats persisted structure as untrusted runtime data at the +CSS boundary. It accepts only finite numeric indentation levels from 1 through +64; invalid, non-numeric, or unbounded values render at level zero without an +inline style. This keeps a malformed or tampered response from becoming CSS. + ## Consequences - Flat display is an explicit unresolved state, not a fabricated hierarchy. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ae62fb8c7..e6e983123 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -359,6 +359,132 @@ The integrated `60b52f26` checkpoint passes the full backend suite: `723 passed, 16 skipped`, with no test failures. Frontend lint, `131 passed`, and production build also pass. Four existing dependency deprecation/security warnings remain non-failing and are not reclassified as product evidence. +## Stale-summary continuity checkpoint: 2026-08-20 + +The private PostgreSQL runtime showed three inspected target rows with summary +contract versions `2`, `5`, and `5`, while the current contract is `6`. The +same aggregate inspection found persisted 5W1H rows for the two newer rows, +but a current-contract read rejected their older summary projections and +re-entered the live LLM path. This made a temporary gateway failure look like +missing product evidence even though the source body and prior projection +still existed. + +ADR 0114 now keeps the default current-contract boundary, but permits the +summary endpoint to return an explicitly labelled stale projection when a +refresh is unavailable or incomplete. The buyer popup shows the saved-summary +state and exposes a retry action; a successful contextual-orchestrator refresh +still performs the only replacement. No private post identifiers or body text +are recorded here. + +## Post-content recovery checkpoint: 2026-08-20 + +The operator backfill path now finalizes the post-content job ledger in the +same transaction as persisted semantic units. A successful synchronous +backfill therefore cannot leave a terminal failed job beside successful +buyer-visible content. + +- A bounded private run processed four authorized posts, persisted 89 + embedding rows, and produced one image description with one region artifact. +- The same run persisted one footnote unit and resolved all inspected text + structure decisions. +- A table-shaped post produced 45 semantic units, including 14 table-row + units, and its ingestion ledger ended in `succeeded` with an operator audit + event. +- These are bounded runtime observations only; they do not establish + corpus-wide parser accuracy or protected-main behavior. + +The explicit terminal retry and ledger-finalization contract is recorded in +ADR 0115. PR #311 remains an active stacked delivery candidate; its required +checks and formal approval must be rechecked at the exact current head before +any protected merge claim. + +## Image locator and buyer table checkpoint: 2026-08-20 + +A first bounded private reprocessing run completed five parent-image +descriptions and five region embeddings, but aggregate inspection found five +full-image boxes, zero decomposed boxes, and four described region rows. Those +boxes were emitted by the locator and were not evidence of five meaningful +regions. After the full-image guard was deployed, a second bounded run +completed five parent-image descriptions, zero region rows, zero region +embeddings, and 21 total unit/image embedding rows. The final aggregate was +zero full-image boxes, zero decomposed boxes, and zero described region rows. +The parent image channel is usable; detailed visual decomposition remains an +unproven capability rather than a fabricated success. + +The normalization path now preserves that distinction: an invalid, partial, or +single full-image locator response keeps parent-image OCR/caption evidence but +does not invent a full-image region row. Buyer OCR that preserves consistent +pipe-delimited rows is rendered as an HTML table, while ordinary OCR remains +readable text. ADRs 0077 and 0110 record these boundaries. The gateway emitted +BrokenPipe errors after timed-out client calls during the bounded run, so the +locator/region latency budget remains an open operational gap. + +## Live protected-gate checkpoint: 2026-08-20 22:30 KST + +The live repository scan found 35 open pull requests, including two drafts, and +zero formal `APPROVED` reviews at their observed heads. Therefore no pull +request is represented here as protected-main truth or as merge-ready. + +PR #270's exact head retained its application checks as successful, but the +external Strix job failed closed after GitHub Models returned HTTP 410 for a +scheduled retirement brownout. The failed job was re-requested directly against +that same head and is queued for revalidation; this is provider availability +evidence, not a source finding and not permission to bypass Strix. + +PR #282's exact-head review request was corrected after an earlier comment +contained a stale SHA. Its duplicate ADR-number repair passed the local full +backend suite (`776 passed, 16 skipped`), while the remote Checks remain queued. + +The central `.github` scheduler source and focused tests confirm that stacked +pull requests can dispatch the required OpenCode review through the central +repository when the same-repository dispatch credential is present. Current +central dispatch runs are queued by Actions capacity; queue presence is not +treated as approval, a successful review, or a merge result. + +This checkpoint is an operational snapshot only. Recompute exact head, formal +review, required Checks, dependency order, and post-merge SHA immediately before +any protected merge. + +## Exact-head validation checkpoint: 2026-08-20 22:45 KST + +The current stacked delivery head `efd4aab2` passed the local validation matrix: +the backend suite completed with `731 passed, 16 skipped`, frontend lint passed, +Vitest completed with `133 passed`, and the production Vite and Storybook builds +completed successfully. These results cover the checked-out stack only; the +remote PR Checks for #311 remain queued and no independent formal approval is +present. + +The current branch is clean and its local HEAD equals the remote branch HEAD. +The product baseline therefore records these results as reproducible delivery +evidence, not as protected-main release evidence. + +## Live browser acceptance checkpoint: 2026-08-20 22:55 KST + +Against the running Compose stack, a Playwright browser completed the synthetic +OIDC login round trip, loaded a 50-item authorized board page, opened the first +post-detail popup, waited for its asynchronous evidence projection, and closed +the popup. The rendered popup contained 14 sections and 15 headings; the +observed `/api/me`, `/api/posts`, and detail/lineage calls returned HTTP 200. +No source identifier, title, body, or person name is stored in this baseline. + +This proves the login-to-list-to-popup interaction for the current local +runtime only. It does not prove that the same journey is available on +protected-main, that private records are universally authorized, or that the +remote PR Checks have completed. + +## Live gate refresh: 2026-08-20 22:54 KST + +The current REST audit still finds 35 open pull requests and 12 open issues; +no current-head independent approval is available for the active merge queue. +PR #311 is open at `82b7b395` with both required Tests checks queued. PR #258 is +open at `49804b0f`; its current `Analyze (python)` check is queued and its merge +state is blocked. Earlier SQL/SAST comments are bound to predecessor commits, +so they are not treated as current-head failures without a fresh matching scan. + + +The current evidence supports continued review/check processing, not a merge +claim. Re-read the exact head, current review commit, all required Checks, and +stack dependency immediately before any protected merge. ### Provider-error checkpoint Provider failures are not buyer evidence. The API and browser boundaries must diff --git a/frontend/src/PostBody.table-source-indentation.test.tsx b/frontend/src/PostBody.table-source-indentation.test.tsx new file mode 100644 index 000000000..0e0794a9a --- /dev/null +++ b/frontend/src/PostBody.table-source-indentation.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { PostBody } from "./PostBody"; + +describe("PostBody table-source indentation matching", () => { + it("does not reuse an earlier table cell when a later paragraph has the same text", () => { + render( + , + ); + + const repeatedNodes = screen.getAllByText("Repeated"); + const paragraph = repeatedNodes.find((node) => node.tagName === "P"); + + expect(screen.getByRole("table")).toBeInTheDocument(); + expect(paragraph).toBeDefined(); + expect(paragraph).toHaveAttribute("data-indent-level", "1"); + }); +}); diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx index 1a9b00c4a..05a96e01c 100644 --- a/frontend/src/PostBody.test.tsx +++ b/frontend/src/PostBody.test.tsx @@ -79,6 +79,30 @@ describe("PostBody", () => { expect(screen.getByText("- 설치 확인")).toHaveAttribute("data-indent-level", "2"); }); + it("drops hostile or unbounded indentation before it reaches CSS", () => { + render( + , + ); + + const paragraph = screen.getByText("Untrusted indent"); + expect(paragraph).toHaveAttribute("data-indent-level", "0"); + expect(paragraph).not.toHaveAttribute("style"); + }); + it("uses persisted indentation for ordinary paragraphs without table markers", () => { render( MAX_RENDERABLE_INDENT_LEVEL + ) { + return undefined; + } + return value; +} + function renderSegment(segment: PostBodySegment, index: number, imageContent?: PostImageContent) { switch (segment.kind) { - case "text": + case "text": { + const indentLevel = safeIndentLevel(segment.indentLevel); return (

{segment.text}

); + } case "image": return renderImageEvidence(index, imageContent, segment); default: { @@ -139,6 +151,10 @@ function isStructuredTableRow(unit: PostContentUnit): boolean { ); } +function structuredTableCells(unit: PostContentUnit): string[] { + return unit.unit_text.split(/\s*\|\s*/); +} + /** * Match a persisted unit to its source-rendering counterpart without relying * on ordinal position. A table row can occupy a persisted non-text unit while @@ -231,12 +247,17 @@ function renderStructuredUnits( rows.push(structureUnits[index]); index += 1; } + for (const row of rows) { + for (const cell of structuredTableCells(row)) { + sourceTextForUnit(cell); + } + } rendered.push( {rows.map((row, rowIndex) => ( - {row.unit_text.split(/\s*\|\s*/).map((cell, cellIndex) => ( + {structuredTableCells(row).map((cell, cellIndex) => ( ))} @@ -247,10 +268,11 @@ function renderStructuredUnits( continue; } const sourceText = sourceTextForUnit(unit.unit_text); + const candidateIndent = safeIndentLevel(unit.indent_level); const persistedIndent = - unit.indent_level > 0 && + candidateIndent && (unit.indent_source_code === "explicit" || unit.indent_source_code === "llm") - ? unit.indent_level + ? candidateIndent : undefined; rendered.push( renderSegment( @@ -290,7 +312,6 @@ export function PostBody({
{splitPostBody(body).map((segment, index) => { const content = segment.kind === "image" ? imageContent[imageOrdinal++] : undefined; - if (segment.kind !== "text") return renderSegment(segment, index, content); return renderSegment(segment, index, content); })}
{cell}