Skip to content

sync-models: naive parseJsonc line-stripper breaks on block comments and string contents — multi-line /* */ or a ' // ' string fails the sync, a ', ]' string is silently corrupted #704

Closed

Description

@dezren39

Problem

bin/sync-models.mjs reads the OpenCode config with a naive line-based JSONC stripper, parseJsonc (lines 224–231), even though the same file already contains a correct comment-aware parser, parseJsoncTree (line 238). parseJsonc:

  • drops only lines that start with // or /*,
  • strips everything after the first whitespace + // on each remaining line, whether or not it is inside a string,
  • removes trailing commas with a whole-text regex (/,\s*([}\]])/g) that also matches inside strings.

Three verified failure modes follow (probe below, run against the verbatim function extracted from current main b382bfb8):

  1. A multi-line block comment makes the sync fail outright. Only the line starting with /* is dropped; the continuation lines remain and JSON.parse throws. main() catches this at the config read (lines 391–398) and exits 1 — the sync-models service fails on every run for a config OpenCode itself accepts (block comments are legal JSONC).
  2. A string containing // makes the sync fail outright. The line stripper cuts the string at the comment-like sequence: {"note": "docs // see guide"} is truncated mid-string and JSON.parse throws "Unterminated string".
  3. A string containing , ] or , } is silently corrupted. {"note": "a, ] b"} parses successfully as {"note": "a] b"} — the comma is deleted from the value with no error. The object parseJsonc returns is the read model the sync decides its adds/updates/removals from (line 393), so a corrupted string can drive wrong edit decisions; the post-edit guard at lines 642–643 calls the same function, so it cannot catch this either.

Controls that work today and must keep working: a URL value ("https://api.example.com/v1" — the // is not preceded by whitespace), ordinary // line comments, and real trailing commas.

bin/sync-models.mjs also has no test file, unlike every sibling in bin/ (agy-proxy, antigravity-ls-proxy, clef-proxy, codex-proxy, design-code-proxy, muse-proxy and proxy-port all have *.test.mjs).

Evidence

bin/sync-models.mjs on current main (b382bfb8):

function parseJsonc(text) {
  const cleanLines = text.split('\n').filter(l => {
    const s = l.trim();
    return !s.startsWith('//') && !s.startsWith('/*');
  }).map(l => l.replace(/\s+\/\/.*$/, ''));
  const cleaned = cleanLines.join('\n').replace(/,\s*([}\]])/g, '$1');
  return JSON.parse(cleaned);
}

Probe (function extracted verbatim with sed -n '224,231p', run under Node):

blockComment          => THROWS Expected property name or '}' in JSON at position 2
stringWithCommentLike => THROWS Unterminated string in JSON at position 14
silentCorrupt         => OK {"note":"a] b","a":1}        (input was "a, ] b")
urlControl            => OK {"baseURL":"https://api.example.com/v1","a":1}
trailingCommaControl  => OK {"a":1}

Inputs: block comment = {\n/* multi\nline comment */\n"a": 1\n}; string cases as shown in the problem section.

Dedup: gh issue list --search "sync-models" returns only #591 (closed, Nix store config-path resolution) — no issue covers the parser. Cross-repo: sync-models exists only in this repo (no copy in the other three clones; OpenChamber's parseJsonc is the jsonc-parser library import in its VS Code package, unrelated code).

Why it matters

sync-models runs as a service against Drew's real ~/.config/opencode/opencode.jsonc, a hand-edited JSONC file. One block comment or one string with // in it stops model syncing entirely with a parse error that points at the config, not the parser; the , ] case is worse — it changes a config value silently and lets the sync act on the changed value.

Acceptance criteria

  • The config read no longer uses the line-based stripper: parse the config with the file's own comment-aware machinery (parseJsoncTree, deriving the value from the recorded tree) or an equivalent string-aware JSONC parse, and use the same parse for the post-edit guard at lines 642–643.
  • A config with a multi-line /* … */ block comment parses to the same object as the same config without the comment.
  • A string containing //, , ] or , } round-trips byte-for-byte through the config parse.
  • Existing good cases keep working: // line comments (whole-line and trailing), real trailing commas, URL values.
  • New bin/sync-models.test.mjs (the script's first test file) pins the cases above; the parsing helpers need an export-for-test seam or equivalent, following the sibling proxies' test pattern.
  • node --check bin/sync-models.mjs passes and a --dry-run against a fixture config exercising those cases completes without a parse error.

Activity

  1. added
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 25, 2026
  2. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    Intake review

    Still live (verified this run, fleet monitor): bin/sync-models.mjs on current main still carries the naive parseJsonc verbatim at lines 224–231 (line-start comment filter + \s+// strip + whole-text trailing-comma regex), alongside the correct parseJsoncTree at line 238 — and there is still no bin/sync-models.test.mjs (contents API 404), unlike every sibling proxy script.

    Related: queued for triage as pilot task 1071 (queue/queued is on this issue; this enrichment does not change its queue state). Sibling test-gap issues from the same review round: #688, #692, #694.

    Notes / risks: failure mode 3 (silent string corruption feeding sync edit decisions) is the dangerous one — a wrong read model can drive wrong config edits, and the post-edit guard calls the same broken function. Modes 1–2 fail loud; mode 3 does not.

    Suggested approach: make parseJsonc delegate to parseJsoncTree (or replace its call sites), then add the missing test file pinning the three failure modes plus the working controls (URL values, line comments, real trailing commas).

    Labels normalized by the fleet monitor: kind/bug, priority/normal, effort/s.

  3. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    effort/sSmall: expected under 2 hours of work
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 26, 2026
  4. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    Triage complete — queue worker (Lane A), task 1071. All three failure modes reproduced on current main (ce1acfb), probing the verbatim parseJsonc extracted from bin/sync-models.mjs:224-231:

    Case Input essence Result
    Multi-line block comment /* multi\n line\n comment */ inside the config object THROW — Expected property name or '}' in JSON at position 7. Only the /* line is dropped; interior lines survive as garbage. In main() (:391-398) this is caught and the sync exits 1.
    String containing // "url": "https://example.com // path" THROW — Bad control character in string literal. The line stripper cuts the string at //, unterminating it. Sync exits 1.
    String containing , ] "note": "x, ]" SILENT CORRUPTION — parses OK but returns {"note": "x]"}: the trailing-comma regex /,\s*([}\]])/g fires inside the string and eats , .
    Controls trailing comma; // line comment Both parse correctly — the naive stripper only breaks on the three shapes above.

    Recommendation: replace the reading parse with the comment-aware scanner that already exists in the same file — with one materialization caveat. parseJsoncTree (:238) parses all three cases correctly in the same probe (its skip() is block/line-comment aware and readString is escape aware), BUT it returns a span tree for comment-preserving edits, not a plain value: object members carry key/value spans, scalars are spans, and the array branch records only start/end. So the fix is not a one-line swap at :393. Sound shapes for the fix: (a) extend the tree parser to materialize plain values (JSON.parse each scalar span; recurse objects; arrays need element values retained), or (b) reuse the scanner logic to strip comments string-safely, then JSON.parse (keeping the legitimate trailing-comma tolerance). Guard behaviour to preserve: main() catches the parse error and exits 1 with [sync-models] Error parsing … — that stays; what changes is that legal configs stop tripping it, and string contents stop being rewritten. A fix packet has been enqueued (small_fix, this queue) — no parser change landed by this triage, per the packet.

    {"v":1,"kind":"note","state":"done","task_id":1071,"summary":"Triaged: all 3 parseJsonc failure modes reproduced verbatim on main ce1acfb4 (block comment THROW, ' // ' string THROW, ', ]' string silently corrupted to 'x]'); controls pass. parseJsoncTree parses all 3 but returns a span tree, not a plain value — fix packet enqueued with materialization caveat.","artifacts":["https://github.com/developing-today/code/issues/704"],"next":"fix task enqueued: replace parseJsonc reading path with comment/string-aware parse (adopt parseJsoncTree + value materialization), add regression tests for the 3 modes"}
    
  5. 9 remaining items

  6. added
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 27, 2026
  7. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Check-in — fleet monitor

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Oct 27, 2026
  9. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: cron repo-fleet-monitor, run 2026-10-27 05:29 CDT
    • Found via: monitor reconciliation
    • Task: Escalation requeue — this item's fix PR has been open with no activity since ~04:20Z, its parked queue task sat ~7h past its s-class deadline, its named resume condition already fired (the clippy: panic! in #1043 MisplacedNode test (markdown.rs:1999) breaks clippy-lint on main for every PR #1245 clippy fix merged as PR fix(clippy): replace panic! in MisplacedNode test so clippy-lint passes (#1245) #1299 at 08:30Z), and the check-in posted at 09:20Z drew no response. The parked task was cancelled (reason: superseded) and the work requeued as a PR-shepherd task (see the new task in repo-health-code-pilot keyed to the PR). Labels reset to queue/queued + status/needs-triage. Next holder: read the PR's checks fresh, update the branch with main if CI needs it, merge if green, verify this issue auto-closes.
    • Expectation: effort/s; done = PR merged and issue verified closed.
  10. added a commit that references this issue on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuestatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions