Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d8b7f56
fix: preserve stale summary buyer continuity
seonghobae Aug 20, 2026
2218ee7
fix: keep post content recovery alive
seonghobae Aug 20, 2026
b7ed772
test(summary): cover stale continuity and retry
seonghobae Aug 20, 2026
c914f76
feat: add explicit post content retry
seonghobae Aug 20, 2026
bff4c9d
test(queue): cover explicit retry rejection
seonghobae Aug 20, 2026
ba60842
docs(queue): complete retry command docstrings
seonghobae Aug 20, 2026
c301e2e
fix: reconcile backfill content with job ledger
seonghobae Aug 20, 2026
4016e2b
fix(ui): preserve indentation after mixed content
seonghobae Aug 20, 2026
20b9177
Merge remote-tracking branch 'origin/fix/stale-summary-buyer-continui…
seonghobae Aug 20, 2026
e219e48
docs: record post content recovery evidence
seonghobae Aug 20, 2026
8b0e1d8
docs(ui): clarify indentation matching boundary
seonghobae Aug 20, 2026
5873966
test(ui): cover missing source unit fallback
seonghobae Aug 20, 2026
4bc21e4
fix: serialize per-post vision region analysis
seonghobae Aug 20, 2026
69b96e9
Merge remote-tracking branch 'origin/fix/stale-summary-buyer-continui…
seonghobae Aug 20, 2026
d9b4b51
fix: preserve honest image evidence rendering
seonghobae Aug 20, 2026
f13bc3f
feat: enrich semantic hints from source catalogs
seonghobae Aug 20, 2026
adbb72d
fix: reject non-decomposed vision regions
seonghobae Aug 20, 2026
766b742
docs: refresh protected gate baseline
seonghobae Aug 20, 2026
4d3d7ae
docs: record corrected image region evidence
seonghobae Aug 20, 2026
85dbfb5
fix: trim source catalog lookup codes
seonghobae Aug 20, 2026
efd4aab
docs: record catalog hint boundary
seonghobae Aug 20, 2026
deca246
docs: record exact-head validation evidence
seonghobae Aug 20, 2026
82b7b39
docs: record live browser acceptance
seonghobae Aug 20, 2026
69b8909
docs: refresh live gate topology
seonghobae Aug 20, 2026
a903fff
Merge remote-tracking branch 'origin/fix/project-bound-summary-action…
seonghobae Aug 20, 2026
d36e106
Merge remote-tracking branch 'origin/fix/stale-summary-buyer-continui…
seonghobae Aug 20, 2026
ab27898
fix: refresh content after summary ingestion
seonghobae Aug 20, 2026
446abeb
Merge remote-tracking branch 'origin/fix/project-bound-summary-action…
seonghobae Aug 20, 2026
36a879b
Merge remote-tracking branch 'origin/fix/stale-summary-buyer-continui…
seonghobae Aug 20, 2026
17ff911
Merge remote-tracking branch 'origin/fix/project-bound-summary-action…
seonghobae Aug 21, 2026
e38df62
Merge remote-tracking branch 'origin/fix/stale-summary-buyer-continui…
seonghobae Aug 21, 2026
9357d38
style: normalize summary test imports
seonghobae Aug 21, 2026
815e0c0
Merge project-bound summary actions into stale continuity
seonghobae Aug 21, 2026
50aa037
Merge remote-tracking branch 'origin/fix/project-bound-summary-action…
seonghobae Aug 21, 2026
a47a7a9
Merge remote-tracking branch 'origin/fix/stale-summary-buyer-continui…
seonghobae Aug 21, 2026
60878ff
Merge commit '7b8b9d556ca53350bced2328e7900933df8fcd5e' into HEAD
seonghobae Aug 21, 2026
338f028
fix(ui): preserve indentation after mixed content (#314)
seonghobae Aug 21, 2026
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
5 changes: 5 additions & 0 deletions docs/adr/0073-llm-structure-adjudication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
126 changes: 126 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/PostBody.table-source-indentation.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<PostBody
body="<table><tr><td>Repeated</td></tr></table><p>&nbsp;&nbsp;Repeated</p>"
structureUnits={[
{
unit_index: 0,
unit_kind_code: "dom",
unit_label: "tr",
unit_text: "Repeated",
indent_level: 0,
indent_source_code: "explicit",
indent_confidence: 1,
indent_evidence: "table row",
},
{
unit_index: 1,
unit_kind_code: "dom",
unit_text: "Repeated",
indent_level: 0,
indent_source_code: "unresolved",
indent_confidence: 0,
indent_evidence: "",
},
]}
/>,
);

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");
});
});
24 changes: 24 additions & 0 deletions frontend/src/PostBody.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<PostBody
body="<p>Untrusted indent</p>"
structureUnits={[
{
unit_index: 0,
unit_kind_code: "dom",
unit_text: "Untrusted indent",
// Runtime JSON is untrusted even though the generated type is numeric.
indent_level: "0; color: red" as unknown as number,
indent_source_code: "explicit",
indent_confidence: 1,
indent_evidence: "untrusted fixture",
},
]}
/>,
);

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(
<PostBody
Expand Down
43 changes: 32 additions & 11 deletions frontend/src/PostBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,36 @@ function renderImageEvidence(
);
}

const MAX_RENDERABLE_INDENT_LEVEL = 64;

function safeIndentLevel(value: unknown): number | undefined {
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
value <= 0 ||
value > 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 (
<p
key={`post-body-text-${index}`}
className={`post-body-text${segment.role === "footnote" ? " post-body-footnote" : ""}`}
data-content-kind={segment.role ?? "text"}
data-indent-level={segment.indentLevel ?? 0}
style={
segment.indentLevel
? { paddingInlineStart: `${segment.indentLevel}em` }
: undefined
}
data-indent-level={indentLevel ?? 0}
style={indentLevel ? { paddingInlineStart: `${indentLevel}em` } : undefined}
>
{segment.text}
</p>
);
}
case "image":
return renderImageEvidence(index, imageContent, segment);
default: {
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
Comment on lines +250 to +254

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Table-cell consumption relies on 1:1 source segment mapping

The loop consumes source segments matching each table cell so a later unresolved paragraph with identical text cannot reuse a cell's segment. This holds because splitPostBody treats each <td> as a block boundary (BLOCK_TAG), yielding one segment per cell. If a source shape ever merged cells into one segment, or a same-text paragraph earlier in source order mapped to a structure unit ordered after the table, sourceTextForUnit could still shift indentation via earliest-unconsumed matching.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

rendered.push(
<table className="post-body-table" key={`post-body-table-${index}`}>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={`post-body-table-row-${row.unit_index}-${rowIndex}`}>
{row.unit_text.split(/\s*\|\s*/).map((cell, cellIndex) => (
{structuredTableCells(row).map((cell, cellIndex) => (
<td key={`post-body-table-cell-${row.unit_index}-${cellIndex}`}>{cell}</td>
))}
</tr>
Expand All @@ -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(
Expand Down Expand Up @@ -290,7 +312,6 @@ export function PostBody({
<div className="post-body">
{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);
})}
</div>
Expand Down