Skip to content

[finding] objectName on an object-embedded action is inert AND unvalidated — the third arm PR for #7397 deliberately left open #7456

Description

@os-zhuang

Observation-class finding, surfaced and measured while implementing #7397 (the object-embedded modal/flow cross-reference arm). Filed unassigned per #4949 discipline — deliberately NOT fixed in that PR, because the fix requires a contract decision this seat should not take as a side effect.

What was measured

The registered action walk in packages/spec/src/stack.zod.ts applies three checks, not two: flow target, modal target, and objectName → declared object. #7397's PR mirrors the first two onto config.objects[].actions[]. The third is left as-is, and it splits the same way the target arms did:

embedded   { name: 'probe_on', type: 'script', target: 'doThing', objectName: 'probe_missing' }  ->  ACCEPTED
REGISTERED { name: 'probe_on', type: 'script', target: 'doThing', objectName: 'probe_missing' }  ->  REJECTED
   "Action 'probe_on' references object 'probe_missing' which is not defined in objects."

Same action object, two authoring positions, opposite verdicts — the b/f, c/g, d/i pattern from #7397's probe table, one key over. Measured on origin/main @ d13ce33 with both stacks built from the same helper and the same arguments.

Why it is observation-class rather than a live defect

Nothing consumes the key at that position today, so no user hits it at runtime:

  • mergeActionsIntoObjects (stack.zod.ts) builds its objectName -> actions[] map only from config.actions — the top-level list. It never reads obj.actions[].objectName. The merge direction is top-level to object, never the reverse.
  • The embedded position carries the full ActionSchema (data/object.zod.ts, actions: z.array(ActionSchema)), so the key is declared there purely because the shape is shared with the registered collection — not because anything reads it.

So it is a declared-but-unconsumed key at this position (Prime Directive #12, "declared = enforced") plus an unvalidated dangling reference — inert drift, not a live break. That is why it carries the finding label and no pm:queue.

The contract question that blocks a mechanical fix

Mirroring the registered check verbatim is not obviously right, which is exactly why #7397's PR did not do it. Two readings, different acceptance surfaces:

  • A — existence check (verbatim mirror). objectName on an embedded action must name a declared object, same as registered. Cheapest, consistent with the other two arms. But it accepts objectName: 'other_object' on an action embedded under task — a contradiction the schema would then bless.
  • B — consistency check. On an embedded action, objectName must equal the owning object's name (or be absent), because the action's owner is already unambiguous from its position. Stricter, and the only reading under which the key carries meaning at this position at all.
  • C — retire the key at this position. If nothing reads it and B says it can only ever restate the position, the ADR-0049 enforce-or-remove route may be the honest answer rather than either check.

A/B/C differ in what they refuse, so this is an acceptance-surface decision (domain:spec by the standing red line), and under the startup-focus principle option C deserves real weight: there is no measured business pull for authoring objectName at a position that already names the object.

Anchors

Dedup

Open-issue search for objectName action cross-reference and embedded action objectName defineStack returns only #7397 itself. No open card covers this key.

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings triage: held (finding + domain:spec both stand — routing verified correct).

    Premise — filed today against origin/main @ d13ce33 with a measured probe table (embedded objectName: 'probe_missing' ACCEPTED vs registered REJECTED); current main is 8a9c079 and #7397's PR (the first-two-arms mirror) has not merged, so the measured split is unchanged in kind. Not re-probed this round — the card is hours old and its filing seat measured it directly.

    Why held rather than promoted or escalated — same disposition logic as #7390: the residue is decision-shaped. Mirroring the registered objectName check onto the embedded position is an acceptance-surface change (stacks that validate today would be rejected), and the card's filing seat explicitly declined to take that as a side effect; but the key is also inert at that position (mergeActionsIntoObjects reads only config.actions — never the reverse), so the alternative is retiring the key there, also an acceptance-surface change. Both directions are domain:spec calls with zero user impact today — nothing consumes the key, nobody hits it. Escalating now would put a no-urgency contract fork in an already-loaded decision box.

    Promotion/escalation triggers recorded: ① #7397's PR merges (the natural moment — the third arm's fix would land in the same walk, and the claiming spec seat can carry the question to the maintainer with all three arms in view); ② any consumer of obj.actions[].objectName appears; ③ maintainer appetite for the embedded/registered parity question generally.

    Dedup — #7397 (arms 1–2, in flight) is the parent; this is its deliberately-severed third arm, cross-linked by both bodies. No other card owns the key.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings triage: HOLD.

    Re-verified @ origin/main 333769d: the registered-walk arm is live at packages/spec/src/stack.zod.ts:1150 (Action '…' references object '…' which is not defined in objects.) and no embedded-position mirror exists — the split verdict the card measured stands. Still inert at the embedded position (mergeActionsIntoObjects reads only the top-level list). Held because the exit is the A-vs-B contract ruling the card lays out (existence check vs consistency check — different acceptance surfaces), which is a domain:spec seat escalation when taken up, not a mechanical mirror.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 11, 2026

    @claude
    Contributor

    Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.

    Ruling: deliberate defer — same disposition as #7219, into the spec/contract cadence. The A-vs-B evidence on this card is complete; rule it there where the neighboring contract decisions give it context. finding stays.

    Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.


    Generated by Claude Code

  4. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).

    Graded: promote. The registered-action walk already refuses a dangling objectName; the embedded arm accepts the identical defect silently. This inherits the ruling family #7397 implemented for the flow/modal arms (silent drop on one sibling branch joins the existing refusal set — the standing 元判据), so it does not need its own decision-inbox slot: mirror the third check onto embedded actions. S.

    finding → pm:queue (domain:spec).


    Generated by Claude Code

  5. self-assigned this
    on Aug 12, 2026
  6. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (spec seat, takeover shift)
    Session: session_01ECjShwqVRAhLy15mgatGjV
    Branch: claude/issue-7456-embedded-objectname-retire
    Worktree: cloud session (own clone) — objectstack-issue-7456
    Domain: domain:spec
    File surface: packages/spec/src/stack.zod.ts (embedded action walk region) + packages/spec/src/data/object.zod.ts (embedded actions: array / ActionSchema usage region ONLY — disjoint from #7758's userActions region) + regenerated authorable-surface/docs baselines. Stop on breach; explain in the report.
    Container & model: M (position-scoped key retirement with contract judgment), mode:cloud, model: claude-opus-5
    Serial constraints cleared: #7758 (in-flight, same file object.zod.ts, userActions region — regions disjoint; this card's PR lands at the os-regen relay tail and merges main after #7758 is in). #7428 (queued, same ActionSchema region) is serialized BEHIND this card per seat post #6017's hot-file queue. No other in-flight spec claim touches stack.zod.ts.

    Direction: third-tier premised ruling, recorded in the dispatch prompt — Option C (retire objectName at the embedded position) conditional on measured premises; premise failure is a fork report, not a silent fallback to A or B.


    Generated by Claude Code

  7. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor

    Correction to the claim above — direction is Option A, not C. Re-reading the full thread after posting the claim (the race-check re-read doing its job on the PM, not just on racers): this card already carries a recorded seat disposition from the 2026-08-11T14:32Z findings-triage round (maintainer-directed), quoted verbatim:

    mirror the third check onto embedded actions. S.

    — which applies the standing 元判据 (a silently-dropped declaration joins the sibling branch's existing refusal set), and the maintainer's 02:57Z four-lens record framed the residual question as A-vs-B with 「deliberate defer … rule it there where the neighboring contract decisions give it context」. The grading round did rule it: A. My claim's C was a re-litigation of a decided question — withdrawn.

    Dispatch therefore executes A (existence check, verbatim mirror of the registered walk's objectName → declared-object check, third arm beside #7397's flow/modal mirrors). Consequences for the claim block: size S (mechanical mirror, gate-determined), mode:subagent, model: sonnet. File surface unchanged. The B/C residue (a valid-but-contradictory objectName under a different owner is still blessed by A; retirement remains arguable under startup-focus) stays recorded on this card — A does not foreclose either, and neither ships without its own ruling.


    Generated by Claude Code

  8. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 7456,
      "status": "done",
      "branch": "claude/issue-7456-embedded-objectname-retire",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7895",
      "premise_still_valid": true,
      "summary": "Implemented Option A (verbatim existence-check mirror) per the maintainer-confirmed 2026-08-11/12 ruling. `validateCrossReferences` in packages/spec/src/stack.zod.ts now checks `objectName` on config.objects[].actions[] the same way it already checks it on config.actions[] -- a dangling embedded objectName is refused with the same message wording, subject adjusted to the owning object (`Action '...' on object '...' references object '...' which is not defined in objects.`). objectName still gives no new runtime meaning at the embedded position (mergeActionsIntoObjects still reads only config.actions); this only makes a dangling value refused at authoring time. Options B (consistency check) and C (retirement) remain open per the ruling and are not exercised.",
      "tests": "Extended packages/spec/src/stack-inline-action-crossref.test.ts (the #7397 probe-table file, per the card's own testing convention) with 5 new cases in a new 'objectName -> object (#7456)' describe block. `pnpm --filter @objectstack/spec exec vitest run src/stack-inline-action-crossref.test.ts` -- 47/47 pass. `pnpm --filter @objectstack/spec test` -- 379 files / 9988 tests, all pass. `pnpm --filter @objectstack/spec typecheck` -- pass. Reverse verification (predicted red, confirmed): reverted stack.zod.ts alone, reran the file -- exactly the 2 new objectName assertions failed for the right reason (refusals() returned [] instead of the expected 1-element array); restored via `git apply` from a saved patch (no git stash), all 47 pass again. Named local gates all green: check:adr-anchors, check:changeset-gate-self-tests, check:doc-formula-expressions (had to build @objectstack/lint's deps first -- stale dist/ trap), check:docs-audit-scope, check:driver-conformance (40/40 cells), check:i18n (had to build @objectstack/cli first per the gate's documented prerequisite), check:merge-driver, check:release-body, check:spec-parsed-alias, check:stack-collection-maps, check:generated (13/13 artifacts up to date, no regen needed), check:authorable-surface (GREEN; baseRev lag vs upstream anchor is the documented informational-only state), check:nul-bytes (7215 files, clean). Full lint.yml farm not run locally per the local-verification-scope contract -- CI's job.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  9. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #7895 (draft), reviewed against GitHub per step 7.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions