Skip to content

feat: parse ladder — three string-ladder rungs for weak-model drift (billion-context#1559) - #477

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-09-28_string-ladder-rungs
Open

ranxianglei wants to merge 1 commit into
masterfrom
2026-09-28_string-ladder-rungs

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Three new rungs on the salvage ladder for weak-model syntax drift, per billion-context#1559 (filed during the v0.1.165 pre-release sweep). All changes confined to src/parse-compress-input.ts; no prompt/tool-surface change, no fixture churn, zero new dependencies.

Background

The 0.0.98 ladder (#470/#471) recovers object-in-slot and nested ranges, and parseObjectValue already routes non-JSON slot strings through the line-form path (splitLineEntries). Missing were the shapes where line-form text lives OUTSIDE any JSON shell:

  • G1 — single-line slot string: {"content": "m00150-m00220 topic: summary here"} → was content-not-array, 0 ranges. Weak models / lossy gateways drop the line break of the taught header+body form; the text after the refs had nowhere to go (a single-line entry must have its body after a \n, so it always failed validation).
  • G2 — top-level bare line-form string: compress("m00150–m00220 Auth\nsummary…") raw, or as a JSON string literal decoding to text → was malformed-json / not-object, 0 ranges. salvageContentEntries requires the "content": [ marker, so shell-less text had no recovery path at all.
  • G3 — truncated stringified args whose content slot held a string: '"{"content": "m1–m5 A\nfull\nm6–m8 B\npar' → was truncated, 0 ranges. The array walker only handles [ slots, so every complete leading block was lost.

Changes

  1. inlineSingleLineEntry (G1): when the line-form path yields exactly ONE part holding no line break, re-home the text after the refs header as the summary body by synthesizing the missing break. Applied to the bare-string paths only (object-slot branch + top-level salvage) — never to array elements, whose single-line header-only form has sibling-adoption semantics (#1001) and stays rejected.
  2. salvageLineFormEntries + finishLineFormSalvage (G2): bare-string recovery with a new diagnostic flag stringSalvage: true (additive optional field on CompressParseDiagnostics). Adopted only when ≥1 valid range is found; otherwise the caller keeps its own verdict.
  3. salvageStringSlotEntries (G3): extracts the surviving string-slot prefix (to the closing quote, or EOF when unterminated), decodes \n, returns the line-form blocks. Never-guess rule: while the slot is unterminated the trailing block may be mid-write and is dropped; an unterminated single block yields nothing. Reuses contentSalvage: true (set only on non-empty recovery), keeping parseCompressArgs: content given as a single entry object is rejected (content-not-array) while the same drift at the top level is tolerated — asymmetric salvage wall #470's flag semantics.
  4. Diagnostic parity: the object-slot line-form branch's content-not-array rejection now carries invalidItems/invalidReasons (previously absent) so hosts can log WHY a shape was rejected.

Behavior changes (old → new)

Shape Old New
Single-line slot string, text after refs content-not-array, 0 ranges ok, 1 range (inline summary, derived topic, unflagged)
same, punctuation-only remainder ("m1–m5 …") content-not-array, 0 unchanged (no letter/digit ⇒ not a summary)
Top-level bare line-form string (raw / quoted literal) malformed-json / not-object, 0 ok, N ranges + stringSalvage: true
Top-level prose without refs malformed-json / not-object unchanged
Truncated stringified args, string slot, complete leading blocks truncated, 0 truncated, N + contentSalvage: true
Truncated stringified args, string slot, single cut block truncated, 0 unchanged (dropped, never guessed)
Canonical array/object forms, #470/#472 shapes ok, flags as before unchanged (pinned tests)
Array-element single-line header-only entry no-valid-ranges unchanged (#1001 adoption intact)

Design decisions

  • Never split on embedded ref pairs in a single line. Prose citations (see m00510–m00520) are indistinguishable from headers; mis-splitting would silently corrupt a legitimate summary. A collapsed multi-block single line therefore recovers the FIRST block only (whole remainder becomes its summary) — under-recovery keeps the receipt honest so the model retries the rest. Pinned by test.
  • G1 requires ≥1 letter/digit in the inline remainder (\p{L}|\p{N}), so "m1–m5 …" stays rejected.
  • Salvage order for unparseable prefixes, most specific first: array objects → string slot → bare line-form. Existing paths keep precedence; nothing that recovered before loses recovery now.

Tests

15 new regression tests in tests/parse-compress-input.test.ts covering every row above, including the deliberate non-recovery pins. Full gate green on this head: typecheck ✓, 1217/1217 tests, build ✓, prettier stable ✓. Before/after verified with the A–K shape matrix from the issue thread.

Release plan

After merge: isolated release v0.0.99 PR (version bump only) → CI publish → verify npm view acp-kernel version → then the billion-context pin-bump PR referencing billion-context#1559.

中文摘要:为弱模型语法漂移补上解析梯级三档(单行槽内字符串内联摘要、顶层裸行式字符串回收、截断字符串槽完整块回收),全部带回归测试,1217/1217 全绿;合并后走 v0.0.99 发布流程,再升 billion-context 钉版本。

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-28_string-ladder-rungs (68a7bb6)

Option A — Install from npm PR tag (recommended)

In your adapter project:

npm install acp-kernel@pr-477

Each push to this PR publishes a new version under the pr-477 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf acp-kernel-pr477.tgz
npm install ./package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review complete. I fetched the branch (c2b4dc7), ran the full gate locally on the head (Node v22.23.2: typecheck clean, tests 1217/1217, build success, format:check stable — matching CI, which is green: test 24/22, build-artifact, pr-validation), and independently re-derived your before/after table by running an identical 13-shape probe matrix against origin/master (5d7b538) and the PR head. Merge-base is the master tip, so no stale-base/rebase concern. Duplicate screen (open+closed issues, open PRs): no overlapping work. Findings below, in review-priority order.

① Direction (original issue) — billion-context#1559 asks for string-in-content-slot recovery in the salvage ladder. Verified against the issue: the approach matches its suggested fix (route through the existing line-form parser; contentSalvage reused for G3, new sibling stringSalvage for G2). One precision note: the issue's literal multi-line example ({"content": "m00150–m00220 topic\nsummary…"}) was already recovered on master by the pre-existing object-slot bare line-form path — my probe confirms ok n=1 on 5d7b538. The true residual gap was the single-line variant (no \n anywhere → one part → rejected "missing summary"), which is exactly what G1 targets. G2/G3 are siblings of the same root cause (line-form text outside any JSON shell), so bundling them here fits one-root-cause-one-fix. Direction: correct, fixes the root cause, not a symptom.

② Regression inventory — behaviors owned by the touched paths, each audited (probe = my A–M matrix; suite = full 1217):

  1. Canonical object-array form → identical (ok n=1, unflagged) — probe-identical old/new.
  2. parseCompressArgs: content given as a single entry object is rejected (content-not-array) while the same drift at the top level is tolerated — asymmetric salvage wall #470 object-in-slot salvage → identical (ok n=1 + contentSalvage).
  3. Truncated canonical ARRAY slot salvage → identical (truncated n=1).
  4. #1001 array-element header-only sibling adoption → identical (no-valid-ranges, 1 invalid) — G1 correctly never touches array elements.
  5. Multi-line slot string (source-issue example) → identical (ok n=1, unflagged).
  6. Top-level prose without refs (raw and quoted) → identical (malformed-json / not-object, unflagged) — no false-positive recovery.
  7. Single cut block in truncated string slot → identical (truncated n=0, unflagged) — never-guess preserved.
  8. Double-stringification unwrap precedence → unchanged (salvage order is most-specific-first: array objects → string slot → bare line-form; suite covers).
  9. Single-quote repair retry → still functional and now less frequent (strictly-more-ranges win rule untouched; G3 single-quote test pins the interaction).
  10. finishSalvage kind semantics → the hardcoded true for the salvage path is provably equivalent to the old looksTruncated(cleaned) (every salvage input is structurally truncated; verified against master source); empty-fallback still passes looksTruncated as before. Zero verdict drift for any previously-recovering input.

No unintended regressions found. Every changed line is additive recovery on inputs that previously yielded 0 ranges.

Behavior changes (old → new, per Deliverables rule 4):

  • G1 single-line slot string: content-not-array/0 → ok/1, unflagged (deliberate — aligns with the existing unflagged multi-line slot-string lane).
  • G2 bare top-level line-form / JSON string literal: malformed-json|not-object/0 → ok/N + stringSalvage:true.
  • G3 truncated stringified args with string slot: truncated/0 → truncated/N + contentSalvage:true.
  • Diagnostic parity (easy to miss): the object-slot line-form branch now populates diag.invalidItems/invalidReasons on the rejection path too (previously success-only). Verdict unchanged; e.g. {"content": "m1–m5 …"} goes inv=0 → inv=1 with the "missing summary" reason. Intentional and disclosed in the PR body; hosts reading these fields gain data, lose none.
  • CompressParseDiagnostics.stringSalvage?: boolean — additive optional field.
  • Judgment call I accept but flag: raw bare top-level text has no structural terminator signal, so a stream-cut bare string yields kind:"ok" with a possibly-trailing partial summary (refs intact — the load-bearing part). Consistent with "never guess, never fabricate"; just noting the asymmetry vs the shell-wrapped G3 lane.

③ Code — escape-aware scanner is correct (\\ skips pairs, unterminated detection, \n decode); anchored ref regexes carry no /g so .exec is stateless-safe; never-guest invariants hold at every rung (cut trailing block dropped, ≤1 block → nothing, punctuation-only remainder gated by /\p{L}|\p{N}/u). One soft observation, not a blocker: the source issue explicitly wanted the flag so "hosts can observe the drift rate", and G1 is unflagged — which means G1 drift becomes less observable than before (it used to show up as content-not-array rejections). My recommendation: leave it unflagged as-is for consistency with the closest precedent (multi-line slot strings, same slot, same payload type, differing only in line count — flagging only the single-line variant would make telemetry depend on incidental line count); if you later want real G1 drift-rate numbers, the right instrument is counting any string-in-slot regardless of line count, which is a separate semantic call. Say the word if you disagree and I'll fold a flag into the branch.

④ Tests — the 15 new tests are real regression tests: exact refs/summaries/kinds/flags asserted, and they pin the non-recovery cases (punctuation-only remainder, prose without refs, single cut block, #1001 array header-only) plus escape decoding and the quote-repair-retry interaction. Each rung regressing would flip an assertion. Valid.

⑤ Diff cleanliness — 2 files, +303/−9, every line serves the feature; no unrelated churn, no lockfile/generated artifacts, no mass whitespace. Clean.

Verdict: ready to merge (human action — I won't merge). Post-merge plan as proposed is sound: isolated release commit release v0.0.99 (version line + matching lockfile lines only) from master, then the billion-context pin-bump referencing #1559 — kernel-first ordering respected.

修复了弱模型漂移产生的三种字符串形态压测参数(单行槽内字符串、顶层裸字符串、截断字符串化参数)被整体拒绝的问题,在解析梯子上补了三档只救确定内容、绝不猜测的恢复;经本地全量门与 13 项前后行为矩阵对照确认无回归,仅有一处已披露的诊断字段增强,可以合并。

@ranxianglei
ranxianglei force-pushed the 2026-09-28_string-ladder-rungs branch from c2b4dc7 to 68a7bb6 Compare October 1, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant