feat(web): shape-based artifact rendering (ArtifactCard) (#796 PR B) - #2314
Conversation
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE
Verified scope, correctness, and integration for task #808 (#796 PR B): shape-driven ArtifactCard rendering.
What changed, and why it's right
ArtifactCardnow dispatches onartifact.artifactType(a value from the closedArtifactShapevocabulary) instead of the olddetectRenderer(data)data-shape sniffing. One renderer per shape —link,commit_set,check,metric,decision,note— plus aGenericCarddefault that works for any shape, known or not.data.kinddrives the icon/label onlink(sokind: 'pr'is a PR row,kind: 'issue'an issue row), and the hardcodedGITHUB_PR_RE/GITHUB_COMMIT_REURL detection is fully removed. This matches the task scope exactly; nothing speculative was added.TaskArtifactsPanelis unchanged and still maps<ArtifactCard>over the run artifacts (panel:585). No other file referenced the removed renderers or oldartifact-card-*test-ids, so there's no collateral.
Verification
ArtifactCardtests: 18/18 (re-seeded with shape-typed artifacts, including an explicit "no GitHub-URL special-casing withoutkind: 'pr'" case).- Full space-component vitest suite: 68 files / 1618 passed, 9 skipped — no collateral breakage.
- Web
tsc --noEmit: clean.oxlinton changed files: clean. CIknip(--include files,dependencies,exports --no-config-hints): exit 0. (TheHasDisplayNameitem only appears under the non-CIknip:alland lives in untouchedpackages/ui— pre-existing.)
Two non-blocking notes (not requesting changes)
- Merge order. This UI degrades to the default
GenericCardfor any row whoseartifactTypeisn't yet a shape. Until #2313 (PR A) lands its backend migration + the legacyresolveLegacyShapeshim, existing'pr'/'result'/'progress'/'review'rows would all render as generic cards. PR A is still OPEN — recommend landing it first. I confirmed the four duplicated shared files (artifact-shapes.ts,mod.ts,types/space.ts,artifact-shapes.test.ts) are byte-identical to PR A's tip (129c8d3), so the rebase drops cleanly. - Cosmetic.
CommitSetCardkeys the commit list by array index (key={i}). Commits carrysha, sokey={c.sha ?? String(i)}would be marginally more idiomatic — but it's a static, never-reordered list, so this is fine as-is.
PR state: OPEN, MERGEABLE, zero unresolved review conversations.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f59355925
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: REQUEST_CHANGES — this supersedes my earlier APPROVE. On re-review (and after validating the four inline findings from the codex review), I agree with all four. The P1 below is a security regression vs. the previous renderer and is blocking.
P1 — Links must restrict URL schemes (security regression) — ArtifactCard.tsx:163 (LinkCard) and ~289 (CheckCard view link)
The old detectRenderer only reached LinkCard for URLs matching isUrl = /^https?:\/\//; everything else (including javascript:) fell through to the table renderer as inert text. The new dispatch keys purely on artifact.artifactType, so a link artifact with data.url = "javascript:…" is now rendered as <a href="javascript:…"> — an actionable XSS anchor (Preact does not sanitize href). Artifact URLs are agent-controlled and agents handle untrusted web/MCP content, so prompt-injection into save_artifact is a realistic path. The check renderer binds the same way.
Fix (restores the old trust boundary):
const SAFE_URL = /^https?:\/\//i;
// LinkCard / CheckCard: render <a> only when isSafeUrl(url); else <span> plain text.Defense-in-depth: also enforce the scheme in validateArtifactShape('link') (shared).
P2 — Crash on malformed commit entries — ArtifactCard.tsx:228-230
shown.map((c) => … str(c.sha) …) dereferences c.sha; c can be null because validateArtifactShape('commit_set') returns ok unconditionally and the payload schema is z.unknown(). A {commits:[null]} artifact throws during render, and there's no error boundary, so one bad artifact blanks the whole artifacts panel. Filter entries first:
const commits = Array.isArray(data.commits)
? (data.commits as unknown[]).filter(
(c): c is Record<string, unknown> => c !== null && typeof c === 'object')
: [];P2 — Minimal PR links are indistinguishable — ArtifactCard.tsx:171-172
For a valid {url, kind:'pr'} with no title/number, the label is just "Pull Request" and the hostname subtitle is suppressed (kind !== 'pr'), so the card shows no URL, repo, or number. With URL-pattern parsing intentionally gone, there's no other identifier. Only suppress the hostname when an identifier is already shown:
{!title && hostname && !(kind === 'pr' && number != null) && (…hostname…)}P2 — Legacy review evidence is dropped (cross-PR) — artifact-shapes.ts:147-148 + DecisionCard
resolveLegacyShape('review') → 'decision', but the producer (node-agent-tools.ts:1071) writes {review_url, cycle, submittedAt, comment_urls?} — no recommendation/summary/counts. DecisionCard renders only those fields, so every review-cycle artifact becomes a bare "DECISION" badge and the review_url link vanishes. (Latent until #2313 wires the migration/classifier, but the logic ships here.) Pick one:
- PR-A fix: map
'review' → 'link'(kind'review') inresolveLegacyShape, since the stored evidence is a URL, not a verdict; or - PR-B fix: have
DecisionCardrenderdata.url || data.review_urlas a link when present, so a decision carrying evidence is never blank.
Not a code finding — CI was flaky. The "All Tests Pass" failure was Daemon Unit Tests (4-space-migrations-a) on migrations 89/96/101, none of which this PR touches. All three pass locally (29/29) and the rerun went green (migrations-a: completed/success). No action needed beyond the re-run.
Merge order still applies: land #2313 (PR A) first so legacy artifactType values migrate to shapes; the four duplicated shared files are byte-identical to PR A tip 129c8d3 (clean rebase). I'm happy to re-review once the four findings above are addressed.
…PR id, review evidence Addresses the four review findings on #2314 (security P1 + three P2s): 1. Safe URL schemes (P1, security): LinkCard/CheckCard/DecisionCard now bind href only for http(s) URLs via safeHref(); agent-controlled javascript:/data:/ custom-scheme URLs render as plain text. Defense-in-depth: validateArtifactShape ('link') also rejects non-http(s) URLs at save time. 2. commit_set crash guard (P2): filter null/non-object commit entries before dereferencing, so {commits:[null]} no longer throws and blanks the panel. 3. Minimal PR identifier (P2): a {url, kind:'pr'} link with no number/title now falls back to the URL as its label instead of a bare 'Pull Request'. 4. Legacy review evidence (P2): DecisionCard surfaces data.url/data.review_url as a 'review' link so review-history rows (mapped review->decision, carrying review_url not recommendation) are not rendered blank. Tests cover all four behaviors plus the new link scheme validation.
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE — all four prior findings are fixed in 45c38b2, independently re-verified this round.
Verified fixes
- P1 — safe URL schemes.
safeHref(url)(http/https-only vianew URL().protocol,nullon empty/invalid) is now the single gate at every anchor site:LinkCard:182,CheckCard:315, and the newDecisionCard:417review link. Non-http(s) URLs render as plain text — no unguardedhrefremains. Defense-in-depth added:validateArtifactShape('link')rejects non-http(s) at save time, with a dedicated test. - P2 — commit_set crash. Entries are filtered to non-null objects before dereferencing;
{commits:[null]}/[42]/[undefined]no longer throw. Thenullcase is covered explicitly (typeof null === 'object', so thec !== nullguard is what excludes it). - P2 — minimal PR identifier. Label fallback now prefers the URL over the kind label, so a bare
{url, kind:'pr'}shows the URL (distinguishable) instead of "Pull Request"; hostname secondary line only when the label isn't already the URL and there's no number. - P2 — review evidence.
DecisionCardsurfacesdata.url || data.review_urlas a "review" link (alsosafeHref-guarded), so legacyreview → decisionrows carry their evidence instead of rendering blank. TheresolveLegacyShapemapping is intentionally left to PR A / producer relocation to PR C — reasonable split.
Independent re-check this round
ArtifactCardvitest: 23/23. Sharedartifact-shapes: 22/22. Webtsc --noEmit: clean.artifact-shapes.tsdiverges from PR A tip129c8d3by exactly the link scheme-validation block — clean single-hunk rebase once #2313 lands.- Threads: 4/4 resolved, PR OPEN and MERGEABLE.
- CI: no failing checks; the previously-flaky
4-space-migrations-a/-bshards both passed this round. Only unrelated tail shards + the aggregate still finishing.
Non-blocking reminders
- Merge order: land #2313 (PR A) first; this branch's
artifact-shapes.tsadds only the scheme-validation lines, so rebase is a trivial reconcile. validateArtifactShape('link')scheme check applies to new saves only — the migration correctly doesn't re-validate legacy rows; the UIsafeHrefis the primary guard regardless.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45c38b2daa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Re: "Keep legacy artifacts renderable until producers migrate" — evaluated; not a PR-B code finding. Recommendation unchanged: APPROVE, with the hard precondition that #2313 lands first.
The facts are accurate and I re-verified them: in this branch save_artifact still persists arbitrary types, the built-in producers still write progress/review/result/pr (node-agent-tools.ts:678, 1071, 1090, 1524, 1562), and resolveLegacyShape/normalizeLinkData/validateArtifactShape are referenced nowhere in packages/daemon/src (only their unit tests call them). So without #2313, every artifact falls through to GenericCard. Agreed.
This is the documented PR A → PR B dependency rather than a defect in this PR's code:
- Task #808 explicitly defines PR B as a follow-up to #796 PR A, whose scope is exactly the
save_artifactshape validation + the backfill migration that wires these helpers. #2313 (PR A) is open and contains that wiring. - The task's chosen architecture is "UI reads shapes directly; legacy is normalized at the write/migration boundary in PR A." Adding legacy normalization back at the UI boundary (the suggested fix) would create a second normalization point that becomes dead code once #2313 lands, and would partially undo this PR's purpose (replace data-heuristics with shape dispatch). It's the kind of speculative bridge code the task deliberately avoids.
The mitigation is merge order — and it's human-gated: #2313 must land before this PR. That requirement is stated in both my review rounds and the approval handoff. If #2313 is at risk of not landing soon, the right move is to hold this PR rather than graft UI-side legacy dispatch onto it. Net: APPROVE stands, conditional on #2313 first.
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE — re-verified on the new head 1fa7ec608 (legacy-type normalization at the UI boundary). This cleanly resolves the codex P1 and makes PR B self-sufficient.
What 1fa7ec608 adds (verified)
resolveArtifactShape():isArtifactShape(type) ? type : resolveLegacyShape(type, data) ?? null. Post-migration rows short-circuit; legacyprogress/result/pr/reviewmap to shapes at the render boundary; truly unknown types still fall toGenericCard.normalizeLinkDatais applied only on thelinkpath (LinkCard keys offdata.url), so a legacypr/result-with-pr_urlrow renders as a proper link.review → decisionkeeps its evidence via theDecisionCarddata.url || data.review_urllink added earlier.- Idempotent and read-only — once #2313 stores shapes, the legacy branch is never taken. This is a bounded type→shape map, not a re-introduced data-shape heuristic, so it stays true to the PR's goal. Legacy data flows through the already-hardened renderers (
safeHref+ commit-entry filter), so no new XSS/crash surface.
Re-check this round
ArtifactCardvitest 29/29 (incl. 6 new legacy-normalization cases: pr→link, progress→note, result→decision, result+url→link, review→decision+link, unknown→generic). Sharedartifact-shapes22/22. Webtsc --noEmitclean.- PR OPEN + MERGEABLE; 5/5 review threads resolved.
- CI: 31 checks, 0 failures (Web Tests + migration shards still finishing at post time).
Merge posture (updated) — this softens my earlier hard precondition: PR B no longer hard-degrades if it lands before #2313, because legacy rows now render via the shape renderers. #2313 is still wanted for closed-set save_artifact validation + the producer migration (so new writes are shapes natively), but it is no longer a rendering blocker. Approve as-is.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1fa7ec6087
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: REQUEST_CHANGES — two more valid P2s from the codex pass, both verified. These are the only open items; after them I expect to approve. (The earlier four findings remain fixed and verified.)
P2 — URL-bearing result rows drop the QA evidence — artifact-shapes.ts:155-162 (routing) + LinkCard
The full-stack QA success artifact (built-in-workflows.ts:1083) is type:'result' carrying a top-level summary and data.pr_url/test_output/ui_changed/browser_validation. save_artifact merges summary into data (node-agent-tools.ts:1519), and resolveLegacyShape('result') routes to link purely because pr_url is a string — so LinkCard renders only the PR link and silently drops the summary/test_output. The old detectRenderer rendered test_output (TerminalOutputCard), so this is a regression for an active producer (the QA workflow itself).
Fix (surgical, PR-B): LinkCard already receives data.summary (it survives normalizeLinkData); surface it as a subtitle when present and there's no title. That keeps the QA verdict visible alongside the link. Full separation (the QA agent emitting a clean decision + link pair) is PR C's producer-relocation scope — note that test_output has no equivalent in the closed shape vocab, so it's genuinely deferred to that work.
P2 — Non-scalar metric values render as [object Object] — artifact-shapes.ts:282-285 (validator) + MetricCard
validateArtifactShape('metric') only rejects null/undefined, and the data schema is z.record(z.unknown()), so {name:'latency', value:{current:42}} is accepted. MetricCard then does String(value) → [object Object], presenting a malformed measurement as valid. Same applies to target (String(target)).
Fix (PR-B): validateArtifactShape('metric') require value (and target when present) to be number/string; MetricCard render value/target only when scalar.
Threads: #2314 (comment) · #2314 (comment)
Everything else from round 4 holds: ArtifactCard 29/29, shared 22/22, typecheck clean, MERGEABLE, CI no failures. Both fixes above are small and contained to this PR's files; ping me for the (final) re-review.
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE — round 5 final sign-off on 3664601ad. Both P2s from the codex pass are correctly addressed; I re-verified from scratch on the pushed commit.
Verified on 3664601ad:
- QA evidence (P2):
resolveLegacyShape('result')now routes url+content results todecision(URL-only stayslink);DecisionCardsurfacessummaryand falls back topr_url(label'view') — so the QA audit keeps both its verdict and its PR link. Blast radius re-checked: the QA workflow is the sole url+content producer; all othertype:'result'rows are url-only (→link, unchanged) or summary-only (→decision, unchanged). PR A's url-only tests still pass. - Non-scalar metric (P2):
validateArtifactShape('metric')rejects non-number/stringvalue(defense-in-depth at save time);MetricCardguardsvalue/targetwithhasValue/hasTarget(renders nothing instead of[object Object]). Existing valid metrics unaffected. - Scope: the commit touches only
artifact-shapes.ts+ArtifactCard.tsx; every changed line traces to the two findings, no scope creep. - Threads: 7/7 resolved, 0 unresolved.
- CI: zero failures.
Lint/Knip/Format/Typecheck✓,Daemon Unit Tests (0-shared-handlers-workflow)✓ (covers thesave_artifactvalidator), flaky migrations shards ✓. 5 daemon integration/online shards still in flight (none touch this PR's files) — confirm green before merge.
Tests (re-run locally on HEAD): ArtifactCard 31/31 (+2), shared artifact-shapes 24/24 (+2), tsc --noEmit clean.
Deferred (acknowledged, not blocking): splitting the QA producer's mixed audit into proper link/check/note artifacts is PR C's scope; this shim preserves the headline evidence (summary + PR link) until then. This PR (shape-based rendering + UI-boundary legacy normalization) is complete and self-sufficient regardless of #2313's merge order.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3664601adc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: REQUEST_CHANGES — this supersedes my prior APPROVE (review 4839430054) and the submit-for-approval that followed it. A valid P2 arrived after I submitted; please do NOT merge until round 7 lands. My autonomy level means a human finalizes the merge — treat this as the current state, not the earlier approval.
P2 — Merged-PR audit renders as an empty card — packages/shared/src/artifact-shapes.ts (resolveLegacyShape hasUrl + normalizeLinkData)
Validated independently. The live post-approval merge template (post-approval-merge-template.ts:208-210, wired in via built-in-workflows.ts:38) writes save_artifact({ type:"result", data:{ merged_pr_url, merged_at, approval_source } }). resolveLegacyShape doesn't recognize merged_pr_url as a URL (it checks url/pr_url/prUrl/review_url only), so the row routes to decision; DecisionCard reads none of {merged_pr_url, merged_at, approval_source} and has no recommendation/summary → a bare "DECISION" badge, hiding the merged PR URL entirely. The previous renderer surfaced it. This is the same omission class as the round-5 QA finding, and notably migrations.ts:10053 already iterates merged_pr_url as a URL field — so this is an inconsistency with the codebase's own canonical URL-field list.
Fix (small, in PR B's UI-shim scope): recognize merged_pr_url as a URL in both resolveLegacyShape's hasUrl check and normalizeLinkData's URL extraction (so it's copied onto data.url). Then the audit routes to link and LinkCard renders the merged PR URL. Cleanest form: align both functions' URL-field set with the migrations.ts:10053 list (['pr_url','merged_pr_url','review_url','reviewUrl','url']) — that covers merged_pr_url and keeps the two sites mutually consistent (and matches the migration). merged_at/approval_source are secondary audit metadata; full producer normalization (a clean link kind:"pr" + audit note) remains PR C.
Thread: #2314 (comment)
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE — round-7 final sign-off on aadbddcd1. The merged-PR-audit P2 is correctly fixed; re-verified from scratch.
Verified on aadbddcd1:
- Merged-PR audit (P2): the post-approval merge audit
{ merged_pr_url, merged_at, approval_source }now routes tolinkandLinkCardrenders the merged PR URL — no more bare badge.findLinkUrlmatches anyurl/*_url/*Urlkey (string + non-empty guarded), coveringmerged_pr_urland stopping the per-variant whack-a-mole that produced both the round-5 and round-7 findings. Shared byresolveLegacyShape('result').hasUrlandnormalizeLinkData(DRY). - Generalization is sound, not over-engineering: two consecutive findings were the same omission class, so a pattern matcher is the simpler long-term choice than re-enumerating. Precedence is correct —
normalizeLinkDataearly-outs on an existingdata.url, so expliciturlalways wins over other*_urlfields;hasUrlonly needs a boolean. - Blast radius contained:
normalizeLinkData/resolveLegacyShapeare called only fromArtifactCard.tsx(web) — the daemon save/migration paths don't use them — so the broader matching affects only UI-boundary rendering. - Scope: commit touches 3 files (logic + shared test + web test); no scope creep.
- Threads: 8/8 resolved, 0 unresolved (no new findings on this commit).
- Tests (re-run locally on HEAD): shared 25/25 (+1), ArtifactCard 32/32 (+1),
tsc --noEmitclean.
CI: 0 failures on aadbddcd1; the deterministic Lint/Knip/Typecheck gate was green on the prior commit and I verified typecheck/lint locally — confirm it green on this commit before merge (still running as of this review).
Deferred (acknowledged, not blocking): merged_at/approval_source audit metadata and the clean producer split (link kind:"pr" + audit note) remain PR C.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aadbddcd14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE (unchanged) — two more codex P2s (r3700015457, r3700015459) are real as behavior but are the deferred-to-PR-C scope documented back in round 5; my approval on aadbddcd1 stands. Surfacing the rationale and a scope decision for the merge authority.
Both findings are the same class — secondary fields of a mixed legacy payload are dropped after shape routing:
- QA success result →
decision:DecisionCardrenderssummary+ the PR link, droppingtest_output/ui_changed/browser_validation. - Merge audit →
link(round-7 fix):LinkCardrendersmerged_pr_url, droppingmerged_at/approval_source.
The headline evidence IS preserved in each case (QA verdict summary + PR link; the merged PR URL). What's hidden is secondary detail the old freeform table renderer displayed.
Why this is PR C, not PR B: the closed 6-shape vocab has no home for these fields — test_output is fundamentally a check (status + counts), and merged_at / approval_source are audit metadata (a note). This was called out in the round-5 review ("test_output is an acknowledged vocab gap; deferred to PR C's producer normalization"). Rendering them inside PR B's generic DecisionCard/LinkCard would either special-case producer field names or add a generic arbitrary-field renderer — both couple generic infra to a specific producer's payload, which is exactly the freeform-data coupling this PR removes. No clean in-scope fix exists (unlike the round-5/7 findings, which had clean routing/URL fixes).
Recommendation: proceed with the merge (squash, no --delete-branch, dev) after CI green; track both in PR C — the QA producer should emit a check (+ link + note) and the merge producer a link + note, instead of mixed blobs. If the merge authority prefers a PR-B interim mitigation (e.g. a generic expandable raw-data fallback on legacy-mapped cards), hold and I'll route it — but I don't recommend expanding PR B's scope, since it re-introduces the freeform coupling the design removes and PR C is the correct home.
…PR id, review evidence Addresses the four review findings on #2314 (security P1 + three P2s): 1. Safe URL schemes (P1, security): LinkCard/CheckCard/DecisionCard now bind href only for http(s) URLs via safeHref(); agent-controlled javascript:/data:/ custom-scheme URLs render as plain text. Defense-in-depth: validateArtifactShape ('link') also rejects non-http(s) URLs at save time. 2. commit_set crash guard (P2): filter null/non-object commit entries before dereferencing, so {commits:[null]} no longer throws and blanks the panel. 3. Minimal PR identifier (P2): a {url, kind:'pr'} link with no number/title now falls back to the URL as its label instead of a bare 'Pull Request'. 4. Legacy review evidence (P2): DecisionCard surfaces data.url/data.review_url as a 'review' link so review-history rows (mapped review->decision, carrying review_url not recommendation) are not rendered blank. Tests cover all four behaviors plus the new link scheme validation.
aadbddc to
b673be5
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (z.ai/glm)
Model: glm-5.1 | Client: NeoKai | Provider: z.ai/glm
Recommendation: APPROVE — re-verified the rebased tip b673be58c after #2313 (PR A) merged and this PR was rebased onto dev (force-push from aadbddcd1). All approved behaviors are preserved; the rebase is clean.
Rebase re-verification — all 6 approved behaviors present at b673be58c:
resolveLegacyShape('result')summary-aware routing ✓ —hasSummary+findLinkUrl→ url-only result =link, summary-bearing =decision. (PR A's version useshasSummaryinstead of PR B'shasContent; semantically equivalent for the real producers — QA result has summary+pr_url → decision; merge audit hasmerged_pr_urlonly → link. The droppedtest_output/stdout/stderrcontent-detectors only affected a hypothetical url+output-no-summary result that no producer emits, andtest_outputrendering is PR C anyway.)validateArtifactShape('metric')rejects non-scalarvalue✓validateArtifactShape('link')http(s) scheme validation (PR-B-only addition) ✓findLinkUrlhelper (PR-B-only) wired intoresolveLegacyShape+normalizeLinkData✓MetricCardscalar guards (hasValue/hasTarget) ✓DecisionCardpr_urlfallback +'view'label ✓
PR A's merged versions also brought sensible refinements (commit_set repo:branch identity, decision kind-namespaced keys, note accepts a bare ts) — improvements, no regressions.
Scope: PR diff vs dev is the ArtifactCard renderer + the two artifact-shapes.ts hardening additions (http(s) validation, findLinkUrl) — exactly as the rebase summary describes. Tests green: shared 25/25, ArtifactCard 32/32. CI is rerunning on b673be58c (was fully green on aadbddcd1) — confirm green before merge.
The two legacy-fidelity items remain deferred to PR C (#809). Note: the codex review bot hit its usage limit, so no further automated findings are expected.
Greptile SummaryReplaces data-shape sniffing (
Confidence Score: 5/5Safe to merge. Every rendered anchor goes through Each shape gets a dedicated sub-component with explicit field access and graceful degradation for malformed payloads. The Files Needing Attention: No files require special attention beyond the
|
| Filename | Overview |
|---|---|
| packages/shared/src/artifact-shapes.ts | Adds findLinkUrl (suffix-based URL field discovery) used in both resolveLegacyShape and normalizeLinkData, plus http(s) scheme validation in validateArtifactShape('link'). |
| packages/web/src/components/space/ArtifactCard.tsx | Full rewrite from data-shape sniffing to artifactType-keyed dispatch. Each shape gets a dedicated sub-component; safeHref guards all rendered anchors; legacy normalization handled cleanly at the UI boundary. |
| packages/shared/tests/artifact-shapes.test.ts | Adds coverage for http(s) scheme rejection in validateArtifactShape('link') and merged_pr_url routing through resolveLegacyShape/normalizeLinkData. |
| packages/web/src/components/space/tests/ArtifactCard.test.tsx | Tests fully replaced to match shape-driven dispatch. XSS guard, DecisionCard multi-candidate URL fallback, legacy normalization, and all six shape renderers are covered. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ArtifactCard receives artifact] --> B{isArtifactShape}
B -- yes --> C[shape = artifactType]
B -- no --> D[resolveLegacyShape]
D -- pr --> E[link]
D -- progress --> F[note]
D -- review --> G[decision]
D -- result URL-only --> H[link]
D -- result with summary --> I[decision]
D -- unknown --> J[null]
C --> K{dispatch on shape}
E --> K
F --> K
G --> K
H --> K
I --> K
K -- link --> L[normalizeLinkData then LinkCard]
K -- commit_set --> M[CommitSetCard]
K -- check --> N[CheckCard]
K -- metric --> O[MetricCard]
K -- decision --> P[DecisionCard]
K -- note --> Q[NoteCard]
J --> R[GenericCard]
K -- null/unknown --> R
Reviews (2): Last reviewed commit: "fix(web): DecisionCard — validate each e..." | Re-trigger Greptile
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (NeoKai)
Model: glm-5.1 | Client: NeoKai | Provider: Zhipu AI
Recommendation: REQUEST_CHANGES (round 11). CI is green; two small DecisionCard logic nits are worth tightening before merge; one Greptile finding is declined as deliberate design.
CI: the earlier red was a transient infra flake (now green)
Finalize Coveralls failed because coveralls.io returned HTTP 503 "Service Unavailable (queue full)" on the parallel-finished webhook — a service-side capacity event, not a coverage threshold or code regression. It self-recovered: all 30 checks now report success (Daemon Unit/Online shards, Web Tests, Lint/Knip/Typecheck, Coverage Quality Gate, Finalize Coveralls, Greptile Review). No action needed on the code for this.
Adjudicating the 3 Greptile P2s (review 4840030306)
r3700636439 — DecisionCard label/href mismatch → please fix. Valid. href = safeHref(str(data.url) || reviewUrl || str(data.pr_url)) picks data.url when set, but the label {reviewUrl ? 'review' : 'view'} keys off whether review_url exists. If a decision carries both data.url and review_url, the anchor points at data.url while reading "review".
r3700636440 — non-http data.url silently drops the evidence link → please fix. Valid, and the more substantive of the two. The || chain selects the first non-empty candidate string, then a single safeHref validates it. A non-http data.url (e.g. a stray ftp://) makes safeHref return null, so the valid review_url / pr_url fallbacks are never evaluated — directly defeating the comment's stated intent ("surface their evidence link to avoid a blank card").
One-shot fix for both — validate each candidate independently and remember which won:
const picked =
safeHref(str(artifact.data.url)) ? { href: safeHref(str(artifact.data.url))!, label: 'view' as const }
: safeHref(reviewUrl) ? { href: safeHref(reviewUrl)!, label: 'review' as const }
: safeHref(str(artifact.data.pr_url)) ? { href: safeHref(str(artifact.data.pr_url))!, label: 'view' as const }
: null;…then render {picked?.href} and {picked?.label ?? 'view'}. These only surface on an undocumented data.url-on-a-decision input (the designed legacy decisions carry review_url or pr_url, not bare url), but the fix is ~6 lines, low-risk, and removes the latent bug from the legacy shim before PR C rewrites it.
r3700636442 — findLinkUrl broad *_url match → declining, deliberate design. This broadening is the round-7 fix, chosen to recognize merged_pr_url and future audit URL keys without per-key whack-a-mole. The image_url/avatar_url→link edge case is benign: such a value is still a valid http(s) URL (gated by validateArtifactShape at storage and safeHref at render), opens correctly, and no current producer emits a result with a bare image_url. Reverting to an explicit list would re-introduce the whack-a-mole the design removed. No change.
Otherwise unchanged on b673be5
Shape-keyed dispatch, safeHref XSS guard, resolveLegacyShape summary-aware routing, validateArtifactShape http(s) gate, and normalizeLinkData remain correct. Shared tests 25/25, ArtifactCard tests 32/32 green. I'll re-approve once the DecisionCard selection is tightened (and the three Greptile threads resolved).
Replace the detectRenderer(data) data-shape sniffing (and the hardcoded GITHUB_PR_RE GitHub-PR special-case) with a dispatch on artifact.artifactType, which after the generic-shapes migration holds a value from the closed ArtifactShape vocabulary. Per-shape renderers: link (icon/label by data.kind — kind:'pr' is a PR row, kind:'issue' an issue row, etc.), commit_set (commit list + +/- totals), check (status chip + counts), metric (name value unit -> target), decision (recommendation badge + summary/counts), note (status text line). A default renderer handles any shape, known or not. Tests now seed shape-typed artifacts instead of relying on data-shape detection. TaskArtifactsPanel is unchanged — it already renders ArtifactCard. Depends on the closed shape vocabulary from #2313 (PR A), included here as commit 4e26d69 since PR A is unmerged.
…PR id, review evidence Addresses the four review findings on #2314 (security P1 + three P2s): 1. Safe URL schemes (P1, security): LinkCard/CheckCard/DecisionCard now bind href only for http(s) URLs via safeHref(); agent-controlled javascript:/data:/ custom-scheme URLs render as plain text. Defense-in-depth: validateArtifactShape ('link') also rejects non-http(s) URLs at save time. 2. commit_set crash guard (P2): filter null/non-object commit entries before dereferencing, so {commits:[null]} no longer throws and blanks the panel. 3. Minimal PR identifier (P2): a {url, kind:'pr'} link with no number/title now falls back to the URL as its label instead of a bare 'Pull Request'. 4. Legacy review evidence (P2): DecisionCard surfaces data.url/data.review_url as a 'review' link so review-history rows (mapped review->decision, carrying review_url not recommendation) are not rendered blank. Tests cover all four behaviors plus the new link scheme validation.
Until the backend producer + DB migration (PR A commit 2) land, daemon rows still carry pre-shape types (progress/result/pr/review) with no backfill, so the shape-based dispatch rendered every real artifact as GenericCard — a regression vs. the old data-shape detection. ArtifactCard now resolves the effective shape at the render boundary: isArtifactShape(artifactType) for post-migration rows, falling back to resolveLegacyShape (progress->note, result->link|decision, pr->link, review->decision) for legacy rows. Link rows also get normalizeLinkData so a pr_url/review_url is copied onto data.url. Truly unknown types still fall to the default renderer. Idempotent: once the backend stores shapes, the legacy branch is never taken. Covered by a new 'legacy type normalization' test block (6 cases).
…alues
Two more automated review findings:
1. Mixed-content legacy results (P2): the full-stack QA workflow writes a result
artifact carrying pr_url + summary + test_output + browser-validation evidence.
resolveLegacyShape('result') was mapping any URL-bearing result to a pure
link, so LinkCard hid the QA summary/output. Refined so a result with a URL
AND content maps to a decision (summary renders); URL-only results stay
links. DecisionCard also surfaces pr_url (label 'view') so the PR link survives.
2. Non-scalar metric values (P2): validateArtifactShape('metric') accepted
{value:{...}} (only checked non-null) and MetricCard stringified it to
'[object Object]'. Validator now requires number|string; MetricCard only
renders scalar values so unvalidated/legacy data can't show [object Object].
Tests: shared (result-with-content->decision, metric rejects non-scalar) +
web (mixed QA result preserves summary + view link; metric object value not
stringified).
… link routing
Round-7 review finding (P2): the post-approval merge audit writes a legacy
result { merged_pr_url, merged_at, approval_source }, but resolveLegacyShape's
url check only knew url/pr_url/prUrl/review_url, so the row routed to decision
and rendered a bare badge — dropping the merged PR URL. migrations.ts already
treats merged_pr_url as a URL field, so this was an inconsistency.
Rather than re-enumerate field names (the codebase has several *_url variants in
artifact data: pr_url, prUrl, review_url, merged_pr_url, image_url, ...), both
resolveLegacyShape('result') and normalizeLinkData now use a shared findLinkUrl
helper that matches any key that is 'url' or ends in '_url'/'Url'. Covers the
migrations.ts list and stops the per-variant whack-a-mole. Existing url-only
tests still pass. Secondary audit metadata (merged_at/approval_source) and the
full producer split stay with PR C.
Tests: shared result+merged_pr_url→link and normalize merged_pr_url; web legacy
merge-audit result renders the merged PR link, not an empty card.
Greptile round-11 findings (both real, edge-case): 1. Label/href mismatch: when a decision carried both data.url and review_url, href resolved to data.url (won the ||) but the label read 'review'. 2. Non-http data.url dropped the evidence link: safeHref(url || reviewUrl || pr_url) validated only the first non-empty candidate, so a non-http data.url returned null and the valid review_url/pr_url fallbacks were never tried. Fix: compute safeHref per candidate and pick the first http(s) one, tracking which won so the link label matches the selected URL. Two new tests cover both cases.
b673be5 to
cce47a9
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
lsm
left a comment
There was a problem hiding this comment.
🤖 Review by glm-5.1 (NeoKai)
Model: glm-5.1 | Client: NeoKai | Provider: Zhipu AI
Recommendation: APPROVE (code) — merge-escalation gated on CI green for cce47a93b.
Round-13 re-review of the rebased head cce47a93b, verified independently from scratch:
- DecisionCard fix correct (
ArtifactCard.tsx:406-418): each evidence-URL candidate is validated independently (safeHref(url)/safeHref(review_url)/safeHref(pr_url)), the first http(s) one is picked, and the link label tracks the winner (view/review). Both round-11 nits are genuinely resolved — r3700636439 (label/href mismatch) and r3700636440 (non-httpdata.urlno longer shadows a valid fallback). r3700636442 stays as deliberate round-7 design. - Tests pass (independent run): ArtifactCard 34/34 (incl. the 2 new cases), shared artifact-shapes 25/25.
- Scope clean: the PR's own diff vs
devis exactly the 4 intended files (artifact-shapes.ts+ test,ArtifactCard.tsx+ test). The largeb673be58c..cce47a93bdelta was only the rebased-in dev content (#2271/#2270) — no scope creep. - All 13 review threads resolved (0 unresolved). Rebased onto dev (0 commits behind); commits signed.
Only gate remaining: CI is still running on cce47a93b (0 failures / 23 in progress), so mergeStateStatus is BLOCKED purely pending checks. I'll escalate for merge once CI completes green and the state clears. (Watch-item: if BLOCKED persists after green CI, a required check may be one skipped on PR branches — I'll investigate then rather than assume.)
Renders workflow run artifacts by shape (
artifact.artifactType) instead of the olddetectRenderer(data)data-shape sniffing, withdata.kindsupplying the icon/label. One renderer per closed shape —link(icon/label by kind;kind:"pr"is a PR row),commit_set(commit list + +/-),check(chip + counts),metric(line),decision(badge),note(line) — plus a default for unknown shapes. The hardcoded GitHub-PR URL detection is dropped (a PR is nowlink kind:"pr"); link URLs are restricted to http(s) at render time.Legacy pre-shape rows (
progress/result/pr/review) are normalized to shapes at the UI boundary viaresolveLegacyShape/normalizeLinkData, so they keep rendering correctly — idempotent once rows are shapes.TaskArtifactsPanelis unchanged (it already maps overArtifactCard).Rebased onto
devafter #2313 (PR A) merged: this is now a clean follow-up containing just the ArtifactCard renderer + twoartifact-shapes.tshardening additions not in PR A — http(s) scheme validation invalidateArtifactShape('link')(defense-in-depth), and afindLinkUrlhelper so legacy URL-bearing results (e.g. the merge audit'smerged_pr_url) route to a link. Two non-blocking legacy-fidelity items (QAtest_output→check; merge-auditmerged_at/approval_source→note) are deferred to PR C.