fix(artifacts): hold back partial <artifact> open tag mid-attribute#95
fix(artifacts): hold back partial <artifact> open tag mid-attribute#95
Conversation
`findSafeFlushPoint` only held the tail back when it was a strict prefix of `<artifact` (e.g. `<art`). A stream split mid-attribute — say `<artifact identifier="a1" type="html"` followed by ` title="t">…</artifact>` — left a tail starting with `<artifact ` but with no `>` yet, so the function flushed the partial open tag as a `text` event. The next chunk began past the open tag and `OPEN_TAG_RE` never matched, so the artifact was silently lost. Also hold back when the tail starts with `<artifact` and the closing `>` has not arrived. Added a regression test covering the mid-attribute split and asserting no `<artifact` substring leaks into a `text` event.
There was a problem hiding this comment.
Findings
-
[Major] False-positive
<artifact...prefixes can stall normal text streaming and grow buffer usage —findSafeFlushPointnow holds back any tail starting with<artifactwhen>is missing, including non-tag text like<artifactual ...that can never matchOPEN_TAG_RE; this prevents incrementaltextemission and can accumulate unbounded buffered text in long streams, evidencepackages/artifacts/src/parser.ts:175.
Suggested fix:const tail = buffer.slice(ltIdx); if ('<artifact'.startsWith(tail)) return ltIdx; const isArtifactOpenStart = /^<artifact(?:\s|$)/.test(tail); if (isArtifactOpenStart && !tail.includes('>')) return ltIdx;
-
[Minor] Missing regression test for non-tag
<artifact...prefixes — the new test validates mid-attribute split recovery, but there is no guard test for plain text like<artifactualto ensure text events still stream and no holdback regression is introduced, evidencepackages/artifacts/src/parser.test.ts:51.
Suggested fix:it('does not hold back plain text starting with "<artifact" but not a tag', () => { const events = collectEvents(['before <artifactual text', ' continues']); expect(events).toEqual([ { type: 'text', delta: 'before <artifactual text' }, { type: 'text', delta: ' continues' }, ]); });
Summary
- Review mode: initial
- 2 issue(s) found in the latest diff.
docs/VISION.mdanddocs/PRINCIPLES.mdwere Not found in repo/docs, so project-level policy cross-check relied onCLAUDE.mdand changed files only.
Testing
- Not run (automation)
open-codesign Bot
| if (ltIdx === -1) return buffer.length; | ||
| const tail = buffer.slice(ltIdx); | ||
| if ('<artifact'.startsWith(tail)) return ltIdx; | ||
| if (tail.startsWith('<artifact') && !tail.includes('>')) return ltIdx; |
There was a problem hiding this comment.
[Major] This condition is too broad: tail.startsWith('<artifact') also matches non-tag text (for example <artifactual ...) that will never satisfy OPEN_TAG_RE. In streaming mode this can suppress text events and grow state.buffer until flush.
Suggested fix:
const isArtifactOpenStart = /^<artifact(?:\s|$)/.test(tail);
if (isArtifactOpenStart && !tail.includes('>')) return ltIdx;| }); | ||
| }); | ||
|
|
||
| it('handles open tag split mid-attribute (between attrs, before final ">")', () => { |
There was a problem hiding this comment.
[Minor] Please add a regression test for non-tag prefixes such as <artifactual to ensure this holdback rule does not delay normal text streaming.
Suggested test:
it('does not hold back plain text starting with "<artifact" but not a tag', () => {
const events = collectEvents(['before <artifactual text', ' continues']);
expect(events).toEqual([
{ type: 'text', delta: 'before <artifactual text' },
{ type: 'text', delta: ' continues' },
]);
});…ng unrelated text
|
Addressed Codex [Major] finding on the The previous guard Tightened to only hold back when the next char is whitespace (attributes coming) or absent (need more bytes to decide). A letter/digit/dash means a different word — flush it. Added 2 regression tests:
Original mid-attribute split test still passes. |
There was a problem hiding this comment.
Findings
- No new issues found in the modified lines with >=80% confidence.
Summary
- Review mode: follow-up after new commits
- Previously reported false-positive holdback risk is addressed by the new next-character guard in
packages/artifacts/src/parser.ts:175, and follow-up tests for<artifactual/<artifact-likeare present inpackages/artifacts/src/parser.test.ts:92andpackages/artifacts/src/parser.test.ts:102. docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs.- Residual risk: parser behavior for malformed
<artifact>variants (for example missing required attributes) is still governed by existing parser rules and not expanded in this diff.
Testing
- Not run (automation)
open-codesign Bot
- packages/artifacts/src/parser.test.ts: biome auto-fix line wrap (formatting regression that slipped in with #95) - packages/core/src/skills/loader.ts: add biome-ignore on parseMapping (score 23) and loadSkillsFromDir (score 19) to unblock pre-push hook for all PR branches. Refactor tracked in issue #94. Both were causing agents to bypass --no-verify or push from temp worktrees. This restores the contract that hooks pass on main.
Summary
findSafeFlushPointin the streaming artifact parser only guarded against tails that are a strict prefix of<artifact(e.g.<art). When a chunk split landed mid-attribute — for example<artifact identifier=\"a1\" type=\"html\"thentitle=\"t\">body</artifact>— the tail already starts with<artifactbut has no>yet, so the function flushed the partial open tag as atextevent. The next chunk began past the tag,OPEN_TAG_REnever matched, and the artifact was silently lost into the chat stream.<artifactand the closing>hasn't arrived. Added a regression test that asserts both the correctartifact:start/artifact:endevents fire and no<artifactsubstring leaks into atextevent.Constraint checks
Test plan
pnpm --filter @open-codesign/artifacts test— 6/6 pass with fix; new test fails on unfixed parser (verified locally)