Skip to content

[finding] The one-value-site promise for CONTRACT_REVIEW_TIER (SKILL.md 「模型升级只改一行一个文件」) is held only by a manual grep — no self-test pins it, so the next model refresh can re-open the drift silently #14616

Closed

Description

@os-litant

Filed unassigned by the os-dev flight on #14303 (session session_01LraLgQVGq8egUwfYZpbYt1), observation only — the fix is not in that PR by scope (the triage on #14303 queued exactly two limbs: bump the constant, make every other site name it).

Blocked-by: #14303

What was measured

Suggested shape (not a decision)

One self-test case in scripts/pm/dispatch-gates.mjs: read every file under .claude/skills/pm-dispatch/** and scripts/pm/**, count occurrences of CONTRACT_REVIEW_TIER's own VALUE (the exact string, read from the constant — never a family/prefix pattern, and never a second literal), and assert the count is exactly 1 at the definition line. Exact-value only: a prefix scan would be the family floor the human floor has not granted, and would also be a second spelling of the model family inside the tool.

Two things the implementer has to respect, both measured before on this file:

  • The tool's watch-hint extraction reads module-body strings and self-test fixture strings as hints (the [finding] dispatch-gates watch hints are read from self-test fixtures and comments, so gates are printed as MATCHED for paths they never read #8478 lineage; the file's inherited-population declaration and the check:watch-hint-literal gate guard that class). Directory names for the scan should be derived from an existing declared population or spelled in a way those guards accept, not added as fresh path literals.
  • Verbatim maintainer rulings in SKILL.md:586 and references/dispatch-runbook.md:17 mention the model family as prose (「Fable 5」, 「fable 5」); they are quotations, not values, and are not matched by the exact-value scan — the case must not be widened until it would touch them.

Refs: #14303 (the drift, and the PR that collapses 16 → 1).

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 2, 2026
  2. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 2, 2026
  3. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage — graded, finding cleared.

    ⚠️ Declared first: this card governs the protocol my own seat runs under. .claude/skills/pm-dispatch/** is the text that defines this seat. Per the #14209 precedent I set state and routing and ⛔ write no recommendation — but note the reason is weaker here than on #14209, because this card asks for a self-test that strengthens a promise, not for a judgement about how strict the seat's own gate should be. Strengthening is not the shape self-interest bends. I am recording the adjacency rather than hiding behind it.

    State

    Blocked-by: #14303 is correct as written — line-anchored, undecorated, pointing at an issue rather than at its PR. #14303 is pm:dispatched (its PR is the one collapsing 16 sites → 1), so the block is live and I have applied pm:blocked here and pm:blocking on #14303 to close the reverse index.

    The ordering is not a formality: this card's whole subject is pinning the count at 1, and the count is not 1 until #14303 lands. A self-test written today would pin 16.

    Why pm:queue on unblock rather than a decision

    Adding one self-test case to scripts/pm/dispatch-gates.mjs is gate strengthening, and the card prescribes its shape precisely rather than asking for a direction. Nothing here needs a maintainer ruling. When #14303 closes, this becomes an ordinary queued card.

    The two hazards the card names are the deliverable, not caveats

    Both are already-measured properties of this file and should be treated as acceptance criteria:

    1. Watch-hint extraction reads module-body and fixture strings as hints (the [finding] dispatch-gates watch hints are read from self-test fixtures and comments, so gates are printed as MATCHED for paths they never read #8478 lineage, guarded by the file's inherited-population declaration and check:watch-hint-literal). Derive the scan's directory names from an existing declared population; ⛔ do not add fresh path literals.
    2. Exact-value only. The card's reasoning is right and worth restating: a prefix or family scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside the tool — i.e. the scan would recreate the defect it exists to prevent.
    3. Consequently the verbatim maintainer quotations at SKILL.md:586 and references/dispatch-runbook.md:17 (「Fable 5」/「fable 5」) must stay out of the population. They are quotations of a ruling, not values. ⛔ Never widen the case until it would match them — that would put a gate in a position to demand edits to a maintainer's recorded words.

    Routing

    domain:skills — both roots this card touches are that lane (the lane table puts .claude/skills/** there whole), and the SUBJECT is the skill's own single-source promise rather than general gate tooling, which is what separates it from the domain:devx gates. tooling.

    priority:p3: once #14303 lands there is no live drift — the constant is at one site and the promise is true. This card buys only that it stays true through the next model refresh. That is real (the 16-site drift #14303 found is precisely what a manual promise looks like one refresh later) and it is not urgent.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Sep 2, 2026
  5. os-litant commented on Sep 2, 2026

    @os-litant
    ContributorAuthor

    Unblocked — Blocked-by: #14303 is cleared: PR #14644 merged at 16:55Z (human merge, os-zhuang) and #14303 is closed; on main the constant's value now appears in exactly one place, so the self-test this card asks for has a true baseline to pin. State pm:blocked → pm:queue in the same stroke. Scope as filed: one check:pm-dispatch-gates case asserting the exact-value count is 1 at the definition line, derived from an existing declared population (the watch-hint guards), never a prefix scan; the two verbatim maintainer quotes stay out of its reach. Dispatches (S, at CONTRACT_REVIEW_TIER — scripts/pm/dispatch-gates.mjs self-tests are a protocol surface) at the next free slot under the maintainer's concurrency of 5.

    Session: session_01LraLgQVGq8egUwfYZpbYt1


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 2, 2026
  7. self-assigned this
    on Sep 2, 2026
  8. os-litant commented on Sep 2, 2026

    @os-litant
    ContributorAuthor

    Claim: PM loop round 5 (pm:queue → pm:dispatched, assignee set in the same stroke)
    Session: session_01LraLgQVGq8egUwfYZpbYt1
    Branch: claude/issue-14616-tier-single-value-site-pin
    Worktree: objectstack-issue-14616
    Domain: domain:skills
    Scope, exactly as filed: ONE check:pm-dispatch-gates self-test case in scripts/pm/dispatch-gates.mjs that reads every file under .claude/skills/pm-dispatch/** and scripts/pm/**, counts occurrences of CONTRACT_REVIEW_TIER's own exact VALUE (read from the constant — never a second literal, never a family/prefix pattern), and asserts the count is exactly 1, at the definition line. Directory names derived from an existing declared population so the watch-hint guards (check:watch-hint-literal, check:declared-population-live, the file's inherited-population declaration) stay green. The two verbatim maintainer quotes (SKILL.md:586, references/dispatch-runbook.md:17) are prose and outside an exact-value scan by construction. A new guard is proved to fail: predict-then-mutate on a scratch copy (a second spelling of the value anywhere in the population ⇒ the case reds), no build leg involved.
    File surface: scripts/pm/dispatch-gates.mjs only (the self-test region). ⛔ .claude/skills/pm-dispatch/**, scripts/pm/check-skill-line-ratchet.mjs and every other file untouched.
    Container & model: S, mode:subagent, model: opus — the correction to the Unblocked comment above: scripts/pm/dispatch-gates.mjs alone is not a fable-mandated glob (--tier derives from the file surface: the mandate table names .claude/skills/pm-dispatch/**, .claude/agents/os-dev.md and the published PM skill); floor sonnet · default opus · ceiling fable.
    Clause-②: no — a self-test, no platform contract; ⛔ no needs:contract-review.
    Serial constraints: PR #14621 (#13680) edits scripts/pm/dispatch-gates.mjs at :7551 / :15280 and awaits merge — disjoint from the self-test tail; the file is not a declared single-writer path. Heavy slots: 5 of 5 after this dispatch.
    Landing form: ONE draft PR against main, first line Fixes #14616; skip-changeset; governed (scripts/pm/**) ⇒ stays draft, human merge; review requests are the seat's step.


    Generated by Claude Code

  9. os-litant commented on Sep 2, 2026

    @os-litant
    ContributorAuthor

    Dev claim (os-dev, dispatched round 5).

    Session: session_01LraLgQVGq8egUwfYZpbYt1
    Branch: claude/issue-14616-tier-single-value-site-pin (pushed empty first as the write-route probe — 200)
    Worktree: objectstack-issue-14616
    Assignee/labels: untouched, per the dispatch — the claim above is this seat's identity bit.

    Scope as filed and as claimed by the PM: ONE check:pm-dispatch-gates self-test case in scripts/pm/dispatch-gates.mjs that walks the two roots, counts occurrences of the tier constant's own exact VALUE (read from the constant at run time — no second literal, no family/prefix pattern) and asserts the total is exactly 1, at the definition line, reporting the offending file:line list when it is not. Nothing else moves.

    Baseline measured on this worktree at 2aa8456c before any edit:

    • exact-value occurrences across .claude/skills/pm-dispatch/** + scripts/pm/**: 1, in scripts/pm/dispatch-gates.mjs (the definition line) — so the case has a true baseline to pin.
    • this file's own watch-hint set, via its own extractWatchHints over its own source: 23 hints. That number is the before-reading for guard 1; the after-reading goes in the PR body.

    Generated by Claude Code

  10. os-litant commented on Sep 2, 2026

    @os-litant
    ContributorAuthor

    os-dev-report

    {
      "issue": 14616,
      "status": "done",
      "branch": "claude/issue-14616-tier-single-value-site-pin",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14672",
      "premise_still_valid": true,
      "summary": "Added ONE self-test case to selfTest() in scripts/pm/dispatch-gates.mjs that walks the two roots (.claude/skills/pm-dispatch and scripts/pm, both derived — dirname of the mandate glob naming the skill's main file, and this module's own directory relative to ROOT), counts occurrences of CONTRACT_REVIEW_TIER's own value read from the constant at run time, and asserts the total is exactly 1 at the definition line, reporting every offending file:line otherwise. No second literal, no prefix/family pattern, no fresh path literal: own watch-hint set is 23 before and 23 after, and the inherited-population declaration is unchanged. Two commits: the case, then a fix after the first battery died on `relative is not a function` — node:path's relative is shadowed by a fixture string inside selfTest(), so the repo-relative spelling now comes from ROOT. PR stays DRAFT (governed scripts/pm/**); assignee and card labels untouched.",
      "tests": "All heavy runs under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-14616; exit codes captured before any pipe; verdicts quoted from each gate's own line. BASELINE at 2aa8456c (pre-edit): `pnpm check:pm-dispatch-gates` -> `✓ dispatch-gates self-test: 1240 cases pass.` AFTER at da479d10 (head sha of the gate run; tree clean, no commit followed): LEG1 exit 0 -> `✓ dispatch-gates self-test: 1241 cases pass.` with the new case's own line `✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)`. ABLATION (no build leg — plain .mjs read from source; predict-then-mutate, prediction written first): mutation is a file inside the population carrying the value, written from the constant at run time, and its landing on disk was CONFIRMED before any reading — MUT_PRESENT=yes, MUT_VALUE_HITS=1, MUT_BYTES=23, and the walk's own file count moved 21+17 -> 21+18. LEG2 exit 1 -> `✗ dispatch-gates self-test: 1 of 1241 case(s) failed.` and the failing line names the offending path `scripts/pm/.tier-value-site-mutation.tmp:1`. RESTORE MEASURED, not assumed: MUT_ABSENT=yes, GIT_STATUS_LINES=0, GIT_DIFF_HEAD_LINES=0, LEG3 exit 0 -> `✓ dispatch-gates self-test: 1241 cases pass.` GATE FAMILY re-derived AFTER the edit by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (stderr names objectstack-ai/objectstack at d5beb9f0; no hand-written path list), 15 commands, all exit 0 except one: check:watch-hint-literal 0, check:declared-population-live 0, check:entry-guard 0, check:parse-guard 0, check-self-test-wired 0, check-ci-filter-parity 0, check-cross-package-test-inputs 0 (both spellings), check-shard-attestation 0, check:agent-test-spelling 0, check:bash32-floor 0, check:cli-command-ids 0, check:pnpm-filter-targets 0, check-nul-bytes 0 (run by standing rule, not in the family). NOT MEASURED: `node scripts/check-test-completeness.mjs` exit 3 — `PREREQUISITE NOT MET`, it grades a saved turbo test log and none was named; its own text says to record it as NOT MEASURED locally and that it is not a red. Repo-wide `pnpm lint` NOT run whole — declared narrowing with its three readings: population from git (`git diff --name-only 2aa8456c HEAD` = 1 path), file count from eslint's own --format json (FILES_LINTED=1, same absolute path, so not a green over zero files), and untouched files immovable because eslint.config.mjs states it `never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file`; ESLINT_EXIT=0, ERRORS=0 WARNINGS=0. CI itself NOT MEASURED — reported at draft-PR time by contract.",
      "mcp_calls": "14 — REST is 403 for repo-scoped reads in this container and gh is absent, so labels went through the MCP fallback (read, union, write, compare-read: size/s + skip-changeset, nothing stripped) and the dedup attempt below used MCP search",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED — handed to PM to file: inside selfTest() in scripts/pm/dispatch-gates.mjs, fixture bindings shadow node:path imports for the whole function scope — `const relative = [...].join('\\n')` at :11023 (function scope) and a `const resolve = () => ...` in a nested block. Any later case calling relative()/resolve() gets a run-time TypeError that `node --check` cannot see; it cost this card one full 4-minute battery. Loud rather than silent (the self-test IS the gate), so observation-class: `finding` label, no assignee, no pm:queue. Reason it is not filed by me: the dedup read could not be validated — repo-scoped REST is 403 in this container, the one targeted MCP search_issues returned 0 results, and the control search that would make that null a reading failed with `API rate limit already exceeded for user ID 314681334`. An unvalidated null is not a dedup, and filing without one is the forbidden shape."
      ]
    }

    Generated by Claude Code

  11. os-litant commented on Sep 2, 2026

    @os-litant
    ContributorAuthor

    Seat review — ACCEPT (in-seat; clause ① surface, Clause-②: no)

    PR #14672, head da479d10, Fixes #14616. Verified on the branch head, not from the PR body:

    • One file, +99 lines, two commits (the case; then the fix that reads the repo-relative spelling from ROOT after the first battery died on a shadowed relative). The constant, .claude/skills/pm-dispatch/** and the line ratchet are untouched.
    • The case walks two derived roots (the directory of the mandate glob naming the skill's main file; this module's own directory), counts the constant's own value read at run time — no second literal, no family/prefix pattern — asserts exactly one site at the found definition line, and reports every offender as file:line. A renamed-away root reports as a failure instead of walking the tree. Watch-hint set 23 → 23; inherited-population declaration unchanged.
    • Battery 1,240 → 1,241; red → green proved on disk: the mutation reached the walk (21+17 → 21+18 files), exactly 1 of 1,241 failed naming the mutated path, and the restore was measured (git status and git diff HEAD empty) before the green leg.
    • Boundary accepted as stated in the case's own comment: the exact-value scan does not reach the maintainer's verbatim rulings (they name the family as prose), and must not be widened until it would.
    • CI: every check on da479d10 green by 18:25Z (Lint & Repo Gates, Type Check ×4 + rollup, Test Core 6/6 + rollup, the three claim guards, Governed Surface Queue Guard, Check Documentation Links, Check PR Size).
    • Hot file: scripts/pm/dispatch-gates.mjs is also held by docs(os-dev): the skip-changeset clause states its criterion as the rule and its list as examples; dispatch-gates prints the same criterion (#13680) #14621 at :7551 / :15280; this case sits in the tier region of selfTest(), disjoint; the later to land merges main in.

    The out-of-scope finding the dev handed to the seat (fixture bindings shadowing node:path's relative / resolve inside selfTest()) is filed as #14688 (finding, unassigned) after a validated dedup (targeted MCP search: 0 results, incomplete_results: false).

    Landing: governed scripts/pm/** ⇒ PR stays draft; review requested from os-zhuang + hotlong. At merge the seat strips pm:dispatched.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:skillsDomain lane — seat card indexed by label:pm:seatpriority:p3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions