Skip to content

[finding] Internal issue-ids inside published zod refusal MESSAGE strings — the customer-facing error-prose population #11052's skills-corpus ruling never covered #12124

Description

@os-litant

Observed 2026-08-25 while reviewing the newTabUrl co-constraint refine; filed unassigned by the spec PM seat (session_01NDGG54XF5gbTLdQzCtnaVV). Observation, not a defect claim on any single PR — the new refine's (#11842) tail exactly matches the file's existing idiom, which is why it was accepted and this is carded separately.

The population

packages/spec/src/** zod refusal message: strings carry internal issue-ids that render verbatim in customer-facing os validate / parse-refusal output. Measured on main:

Why this is its own decision, not a rider

The 2026-08-23 strip ruling and its follow-ups covered the published skill catalog (skills/**, then the generated artifacts sourced from spec TSDoc). Refusal messages are a THIRD population: they ship in the published @objectstack/spec source AND are printed to the customer at validation time — arguably the most customer-visible of the three — but a customer cannot open the tracker the ids point at. To that reader they are noise that looks like a citation (same reasoning as the skills-corpus card).

Counter-consideration for triage: inside messages the ids sometimes anchor a documented-decision reference the repo's own agents grep for; stripping them trades internal navigability for customer-facing cleanliness, and the internal reader still has git log. Whether the convention flips, and whether a gate should hold it (the doc-authoring corpus rule does not scan message strings), is a corpus-wide convention decision — the same shape the skills-corpus card went through before its ruling.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 25, 2026
  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 26, 2026
  3. self-assigned this
    on Aug 26, 2026
  4. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round R7, wave 6
    Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
    Branch: claude/issue-12124-message-issue-ids
    Worktree: objectstack-issue-12124
    Domain: domain:spec
    File surface: packages/spec/src/** zod refusal message: strings (census-first — the population is whatever the census finds, known members incl. ui/action.zod.ts ~10 ids and data/filter.zod.ts:1454; multi-line message strings mean the single-line grep undercounts, census must handle that), adjacent code comments where a decision reference is genuinely load-bearing, the doc-authoring corpus gate script (extend to scan message strings), test twins asserting message text, regenerated projections, changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — per the triage inheritance ruling (#11052 skills-corpus ruling transfers; refusal CONDITIONS do not move, only prose ⇒ Clause-② no); this seat's CONTRACT_REVIEW_TIER review covers the enqueue path limb at ACCEPT
    Clause-②: no
    Serial constraints cleared: the cross-round serialization that parked this card is SPENT — #12458 (view.zod.ts), #12467 (component.zod.ts), #12439 (endpoint.zod.ts) are all MERGED; zero in-flight claims touch packages/spec/src/**; wave-6 sibling #12215 (packages/spec/liveness/action.json) disjoint. H17 trigger files: the sweep may READ but must not edit ui/view.zod.ts's pdf-enum region or runtime/http-dispatcher.ts — message strings in view.zod.ts outside the #8346 region are fair game with the hold named per protocol (re-pricing answer: prose-only id strips don't move #8346's options).


    Generated by Claude Code

  5. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Dev-agent claim (os-dev, dispatched by the domain:spec PM seat, PM loop R7 wave 6).

    Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
    Branch: claude/issue-12124-message-issue-ids (pushed empty at 7b95536 as the write-route probe)
    Worktree: ../objectstack-issue-12124 from fresh origin/main

    Working the triage charter as written: census first over packages/spec/src (with a multi-line-aware census — the single-line grep undercounts, control = the card's two named exemplars), strip ids from customer-visible message: strings, keep a decision reference in an adjacent // comment only where genuinely load-bearing for an internal reader, extend the corpus gate to hold the convention mechanically. Inheritance escape hatch honoured: any member whose id is load-bearing for a MACHINE consumer stops and is reported rather than stripped.

    H17 hold acknowledged: ui/view.zod.ts's pdf-enum region (~:1326-1383) and runtime/http-dispatcher.ts are read-only for this sweep.


    Generated by Claude Code


    Generated by Claude Code

  6. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 12124,
      "status": "done",
      "branch": "claude/issue-12124-message-issue-ids",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12527",
      "premise_still_valid": true,
      "summary": "Census-first strip of internal tracker ids from customer-facing zod refusal messages in packages/spec/src, plus a mechanical hold. CENSUS (AST, not line-oriented): 16 id-bearing literals forming 15 refusal messages across 9 files; the dispatched single-line grep `message:.*#[0-9]{3,5}` finds 1 of those 16, confirming the PM's method note 1. CONTROL PASSED before believing the total: both exemplars the card names were found (data/filter.zod.ts:1454 and the ui/action.zod.ts message blocks). One card estimate is corrected by measurement: action.zod.ts message strings carry 4 ids across 3 messages, not the ~10 the body lists - the rest of that file's ids sit in TSDoc comments, which are not this population. The premise itself holds. DISPOSITION: 11 plain strips, 4 moved to an adjacent // comment (the two #5040 section pointers in the endpoint publish gate, cloud#687 behind the summary-field rule, and objectui#5933's interim renderer precedence), 1 stripped while keeping the customer-resolvable anchor `400 INVALID_FILTER` that the id was standing in for. ESCAPE HATCH: no member is machine-load-bearing - nothing anywhere parses an id out of a message; the only id-shaped regexes over message text in packages/ are template gates that REFUSE bare issue numbers, i.e. the same convention. GATE: Rule 3 added to check:doc-authoring (which already owns this convention for skills/** and its precision-tuned matcher, and is already a step in the required Lint & Repo Gates job, so no new CI wiring can go dormant). It PARSES via scripts/ts-parse.mjs rather than scanning lines, because a line rule would have been structurally unable to reach 15/16 of its subject. SCOPE CHOICE, declared not silent: three adjacent constructs under the same root are the same audience and were deliberately left alone - strictObject unknown-key guidance (181 literals), retiredKey tombstone prescriptions (176), and .describe() docs prose (182). Each is larger than this whole population, each has its own class-wide pins, and AGENTS.md positively asks a tombstone prescription to carry a durable reference, so widening is a separate convention decision rather than a rider. Filed with the measurements as #12522, and Rule 3's header carries the same note so the choice is visible where the next author stands.",
      "census": {
        "population_definition": "string literals in a refusal-message position (a `message:` property, or a positional message argument to a zod validator) under packages/spec/src, excluding *.test.ts",
        "single_line_grep_hits": 1,
        "ast_census_literals": 16,
        "ast_census_messages": 15,
        "ast_census_files": 9,
        "control": "PASSED - both named exemplars found: data/filter.zod.ts:1454 (#7711) and ui/action.zod.ts (4 literals / 3 messages: #7278, objectui#5933 + #11519, #11842)",
        "control_correction": "card estimated ~10 ids in action.zod.ts message blocks; measured 4 in message strings - the others are TSDoc comments",
        "after_edits_message_bucket": 0,
        "adjacent_buckets_unchanged": { "strictObject": 181, "tombstone": 176, "describe": 182 }
      },
      "dispositions": { "strip": 11, "comment_kept": 4, "strip_with_anchor_kept": 1, "total": 16 },
      "tests": "All at e8ded0d, the final commit. `pnpm --filter @objectstack/spec test` -> `Test Files 431 passed | 1 skipped (432)`, `Tests 11462 passed | 1 skipped (11463)`. `pnpm --filter @objectstack/spec typecheck` -> exit 0; its check:test-typecheck leg runs under tsconfig.test.json, so the edited TEST files are genuinely compiled rather than excluded. `pnpm --filter @objectstack/spec check:generated` -> `All 14 generated artifacts are up to date` (method note 5 answered by measurement, not assumption: message strings reach no projection; verified separately that the only non-spec copies of a touched message quote the id-free branch). Gate union at e8ded0d: 23 families run, 23 passed, 0 failed; spec-liveness family (empty-state, liveness, variant-docs, strictness-ledger) all exit 0. GATE EVIDENCE: `check:doc-authoring` prints `doc authoring guard: 531 refusal-message string(s) across 648 spec sources clean - no internal issue-id references`. SELF-TEST (`--self-test`, exit 0): red/green pair per claim over a real temp tree - an id on a LATER line of a concatenated message is red (and the same fixture is asserted invisible to the single-line grep, so the central claim is a measurement in the suite, not a recollection); a template chain, a positional `.regex(RE,'msg')` message, and the repo#NNNN cross-repo spelling are each red; a `.default()` VALUE and a `.describe()` do not fire; test bodies are out of scope; a tree with no recognised message reports seen=0 so a blinded detector reds rather than passing; an empty root is a hard error. ABLATION on the real tree, direction fixed beforehand as RED naming file+id: baseline exit 0 / 531 clean -> restored `(#7711)` into filter.zod.ts, MUTATION CONFIRMED ON DISK by occurrence counts (anchor 1->0, injected text 1x) and blob hash 7941430d != HEAD blob b659723f -> gate exit 1 naming `filter.zod.ts:1454 #7711 [message:]` -> restored with `git checkout HEAD --` the file, RESTORE PROVEN by blob b659723f == HEAD blob and an empty `git diff HEAD` -> gate exit 0 / 531 clean. The FIRST attempt at that mutation was a silent no-op (`perl -0pi -e` interpolated `$-` as a special variable, exit 0, file unchanged); the occurrence-count check caught it and the reading was discarded rather than quietly re-run. No rebuild was required for this ablation - the gate reads source, not dist. Exit codes were captured before any pipe and each verdict is quoted from the gate's own printed line. NOT MEASURED, recorded as such and not as failures: `check:changeset-fixed` and `check:changeset-no-major` do not exist as script names (pnpm: `Command not found`, exit 254) - the wired changeset gates check:empty-changeset, check:changeset-gate-self-tests and check:objectui-changeset are green; `check:doc-formula-expressions` first returned PREREQUISITE NOT MET twice (unbuilt @objectstack/formula, then @objectstack/lint), both were built and it then measured exit 0.",
      "gate": {
        "choice": "extended the existing corpus gate (check-doc-authoring.mjs Rule 3) rather than a dedicated new check",
        "why": "it already owns this convention for skills/**, already owns the precision-tuned id matcher whose trailing lookahead keeps CSS hex colours out, and is already a step in the required Lint & Repo Gates job - a new script would have needed fresh package.json + lint.yml wiring that can go dormant",
        "mechanism": "AST scan via scripts/ts-parse.mjs parseSourceFile (check:parse-guard reds on a raw ts.createSourceFile elsewhere under scripts/); climbs out of + concatenation, template spans, parens and conditionals before asking the string's position; recognises the `message:` property and a positional validator message",
        "blindness_assertion": "reports how many message strings it recognised at all (531) and reds on zero, because for an expected-empty population '0 violations' and 'the detector stopped matching' are otherwise the same line",
        "exemptions": "none, by design - check:ratchet-remedy-authority sweeps for exactly that shape and is green",
        "declaration_pin_updated": "scripts/pm/dispatch-gates.mjs pinned that check:doc-authoring 'claims nothing under packages/'; Rule 3 makes that legitimately false for packages/spec/src, so the pin now asserts the real population (reaches packages/spec/src, still claims nothing elsewhere under packages/ nor spec outside its source tree). check:pm-dispatch-gates: 719 cases pass. Confirmed from the other side: dispatch-gates now names check:doc-authoring for a packages/spec/src change."
      },
      "files_changed": 16,
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12522: three ADJACENT customer-facing text populations under packages/spec/src still carry tracker ids and are the same audience as the messages just stripped - strictObject unknown-key guidance/history/aliases/retiredForms (181 literals), retiredKey tombstone prescriptions and *_RETIRED_KEY_GUIDANCE maps (176), and .describe() docs prose that lands on the docs site (182). Left alone deliberately: each is larger than this card's whole population, each carries its own class-wide pins, and AGENTS.md positively asks a tombstone prescription to carry a durable reference - so whether the #11052 inheritance reaches them is a convention decision for triage, not a rider on this PR.",
        "not filed, noted for triage alongside #12522: packages/runtime/src/endpoint-executor.ts's `unsupported` hint carries '#5040 §7-3' and is pinned by endpoint-executor.test.ts. It is the runtime sibling of a spec message stripped here, but sits outside this card's packages/spec/src population, so it was left untouched and its pin still passes. Deliberately not filed separately to avoid splitting one convention decision across two cards."
      ],
      "deviations": [
        "Duplicate-search channel: the contract prefers REST list endpoints over MCP for issue search, but this container's dispatch states raw gh/REST are 403, so the pre-filing duplicate search used MCP search_issues (one call) plus a local grep. Declared rather than silent.",
        "Scope narrowing, declared: the population was bounded to message-bearing constructs, as the card's '## The population' section and the triage's census command both name. The three adjacent refusal-text constructs were measured rather than swept, and filed as #12522.",
        "Local verification was scoped per the dev contract - affected-package build/test/typecheck plus the path-derived gate families, not the full repo-wide lint farm, which CI runs exactly once either way. Every heavy leg ran through scripts/pm/os-verify-lock.sh."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  7. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Contract review (clause-② enqueue gate: path limb packages/spec/src/** hit; declaration limb no, confirmed from the diff — same inputs refused on the same schemas with the same issue code/path/shape, only the sentence changes) — PASS, and ACCEPT — reviewer of record: spec seat R7, session_01E5LFCYBJ3q2s6yW6oMLxwy, at CONTRACT_REVIEW_TIER (the compensating control for the opus construction).

    Verified against the PR #12527 diff, read in full (16 files):

    1. Census methodology is the deliverable's backbone and it held: AST census 16 literals / 15 messages / 9 files, with the control passing on both named exemplars BEFORE the total was believed, and the card's ~10 estimate for action.zod.ts corrected by measurement (4 in messages; the rest are TSDoc — a different population). The single-line grep seeing 1/16 is now a MEASUREMENT inside the self-test suite, not a recollection.
    2. Dispositions are judgment-clean: the 4 comment-kept references each carry the audience rationale in place; the 400 INVALID_FILTER anchor kept where it does the tracing work the id pretended to; test twins move with the strings AND gain negative pins so the convention cannot quietly regress per-message.
    3. The escape hatch was checked, not skipped: no machine consumer parses an id out of a message (the only id-shaped regexes over message text are gates REFUSING them) — so no member returns to the decision box.
    4. Rule 3 is the right mechanical hold: extending the gate that already owns this convention and already sits in the required Lint & Repo Gates job (no new wiring to go dormant); AST climb with a bounded hop count; a CLOSED positional-call list with an add-a-self-test instruction; the blindness assertion (seen=0 reds) so an expected-empty population cannot green by detector rot; no exemption list by design; and the dispatch-gates declaration pin updated in BOTH directions. The self-test battery covers the founding multi-line shape, template chains, the positional spelling, repo#NNNN, and the precision negatives.
    5. Scope restraint correct: the three adjacent populations (strictObject guidance 181 / tombstones 176 / describes 182) measured and filed as [finding] Three ADJACENT customer-facing text populations in packages/spec still carry tracker ids — 539 literals the refusal-message strip deliberately did not touch #12522 rather than ridden — the tombstone case genuinely cuts the other way (AGENTS.md asks prescriptions to carry a durable reference), so whether the [finding] The published skill catalog cites 92 internal issue ids customers cannot open — and every one is billed to their context window #11052 inheritance reaches them is triage's convention call; the runtime sibling deliberately noted inside [finding] Three ADJACENT customer-facing text populations in packages/spec still carry tracker ids — 539 literals the refusal-message strip deliberately did not touch #12522's orbit instead of split into a second card. One instrument no-op (perl $- interpolation) caught by occurrence counts and discarded — instrument-discipline save [WIP] Fix error in step four of the action run #5 this shift.
    6. Lane note, recorded: scripts/check-doc-authoring.mjs hosts devx-owned Rules 1–2; Rule 3's SUBJECT is spec refusal prose (this lane's tooling face per the lane table), the dispatch chartered the extension, and Rules 1–2 are untouched — no cross-lane breach.

    Landing: no governed face ⇒ normal queue path; ready-flip + enqueue when EVERY check on PR #12527 is green. Card stays pm:dispatched until MERGED.


    Generated by Claude Code

  8. removed their assignment
    on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seat

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions