Skip to content

service-automation: a declared override in <base>.custom is the packaged flow — armed over the code package, rebound in one reload, one ledger switch #28412

Closed

Description

@objectstack-fleet

epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848

Filing gate: ④ child of epic #28236 (P1, cut after ADR-0146's rewrite merged in #28295); follows #28390 ruling A (comment 6542177429: only the paired <base>.custom declares an override), not ADR-0146 D9 item 1's "or from any package in the closure", which that amendment narrows. ⛔ Not a claim.

Blocked-by: #28405, #28410, #28409

Part of #28236 (P1, child N-11) · governs: ADR-0146 D9 items 1–3, Supersede rows ADR-0126 D1 / D4 wall 1 / §7.1 · lane: domain:server (packages/services/service-automation, the flow view in packages/metadata-protocol) · size: L.

Business reason. Today changing HotCRM's flow means cloning it, and both copies run. With this card the customer's version in hotcrm.custom is HotCRM's flow: one run per change, the same switch, and deleting it restores the original.

Measured (75ecaf4ad8).

  • service-automation/src/flow-precedence.ts#resolveFlowPrecedence ranks package (0) over runtime (1); classifyContender makes every stored row runtime whatever its stamps; the remedy printed is "clone the flow under a new name, or switch the packaged flow off" (:306).
  • metadata-protocol/src/protocol-lock-state.ts#declinesStoredRow / #isShippedFlowName keep every stored row of a shipped flow name out of the protocol's flow view, so the kernel:ready / metadata:reloaded syncs (plugin.ts#resolveFlowContenders) never see one.
  • engine.ts:4511 re-binds a name in one synchronous step (deactivateFlowTrigger then activateFlowTrigger); plugin.ts#resyncFlowsFromProtocol withdraws names absent from the fresh view after registering.
  • engine.ts#packagedFlowOwner reads the loader's set, never body stamps; the ledger is unique on (metadata_type, name) (sys-metadata-activation.object.ts:189).
  • A flow row of a shipped name in a writable package is refused by refusePackagedBaseOverride.

Wanted.

  1. Declared rank. A contender is a declared override only when its stored binding (the package_id the protocol read, ⛔ never the body's _packageId) is <owner>.custom (N-08's constant) and its body declares overrides.package = the owner — N-10's exported predicate, not a copy. It ranks over the loader's body. Any other declarer is undeclared and stays shadowed and warned.
  2. The flow view admits it. isShippedFlowName / declinesStoredRow let exactly that row into the execution view, through N-10's served-override predicate. ⛔ No second predicate.
  3. One reload. Every arming step (boot pull, kernel:ready, metadata:reloaded, the metadata-mutation sync) arms the override in place of the base under the one name. Deleting the override re-arms the base in the same step, never withdraw-then-re-add.
  4. Ledger identity. packagedFlowOwner(name) still answers the base. One ledger row switches whichever body is armed; the toggle door, the §7.3 subflow guard and the FLOW_DISABLED consult treat the override as the packaged flow.
  5. Write gate. N-09 lands with flow held closed (its Not here); this card opens it for the .custom declaration in that same gate. ⛔ No second gate.
  6. The undeclared remedy names the declaration in <owner>.custom or a new name, not the clone.

Tests. New cases in flow-name-shadowing.test.ts, flow-precedence-loader-set.test.ts, flow-sync-one-precedence.test.ts: declared .custom row armed; a dependent's declaration or a body-stamped _packageId not armed. One record change fires exactly one body across publish and delete of the override. Ledger off silences the override and on re-arms it. With N-12, a run suspended on the base finishes on the base after the override arms (the epic check 「流程定制只跑新定义、挂起运行按旧版完成」).

Not here. Pinning (N-12); Studio "Customize this flow" (N-18); "new similar" (N-21/N-22); action override (D9, out of this epic).

Dedupe. Related, closed: #11665 (clone + takeover design), #12563 / #12028 (shadowing sentence, banner). Searched flow shadowing, flow precedence shadowed: none arms a declared override.

Links back: #28236, #28390, ADR-0146 D9, ADR-0126 §2/§7.1/§7.3, ADR-0009.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Claim: PM loop round 1 · card thread · 2026-11-01T12:17Z
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01FEkQ5oTRCUNQ4EA3YV84sZ
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-jqgfyc (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: 「28412 插队,现在就派」 (Leehom, 2026-11-01T12:11Z); triage unblock record 6557603861 (p1, pm:queue). Server seats 2 and 3 were told before this claim.
    File surface: packages/services/service-automation/src/ (flow precedence, the arming syncs in plugin.ts / engine.ts, the shadowing remedy sentence) and its three named test files; packages/metadata-protocol/src/ (protocol-lock-state.ts flow view through isServedOverrideRowOf, protocol-packaged-base-refusal.ts opening flow in isOpenOverrideType) and their tests; changesets on the two packages. ⛔ No packages/spec/** shape change, ⛔ no docs/adr/** hunk.
    Container & model: S, mode:thread, model and effort as the thread was opened
    Clause-②: yes (expected: a declared flow override refused today becomes admitted, and the flow precedence changes); contract review owed on the final head before landing
    Metadata-protocol: measured by the dev
    Responsibility: the code is the flow precedence and arming path of @objectstack/service-automation and the override write gate and flow view of @objectstack/metadata-protocol · the platform path is a customer's <base>.custom flow declaring overrides.package = the base (ADR-0146 D9) · reached by every flow save, reload and trigger of a shipped flow name
    Thread-read: none
    Premise: holds (read 2026-11-01T12:17Z at origin/main 0b6a52e8a0: isOpenOverrideType in protocol-packaged-base-refusal.ts refuses a declared flow with declared_type_not_yet_available; classifyContender in flow-precedence.ts ranks every stored row runtime under the package; the remedy sentence says "clone the flow under a new name"; isServedOverrideRowOf is exported from served-override.ts)
    Serial constraints cleared: open PR #28651 (console connector connections) touches only service-automation/src/connector-credentials-ui*.ts, which is outside this surface; merge origin/main once if it lands first. No other open PR touches metadata-protocol/src/ or service-automation/src/.
    Resume-from: none — fresh from origin/main 0b6a52e8a0

    State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
    os-dev-report
    {
    "issue": 28412,
    "status": "done",
    "branch": "claude/project-thread-jqgfyc",
    "pr": "#28693",
    "session": "session_01FEkQ5oTRCUNQ4EA3YV84sZ",
    "premise_still_valid": true,
    "summary": "A flow saved into the shipping package BASE paired BASE.custom under the base flow name, declaring overrides: { package: BASE, basedOn }, is now admitted by N-09 judgeOverrideWrite (isOpenOverrideType opens flow through a CONDITIONS_DELIVERED set; action is still not yet available), served by every read in the base slot via N-10 isServedOverrideRowOf on the stored package_id (declinesStoredRow now takes the row), and armed by the boot pull, kernel:ready, metadata:reloaded and the mutation sync in place of the base through a new override precedence rank read off the stored binding (the registry hydration records package_id on the hydrated flow body as a non-enumerable Symbol.for property: bindStoredRow / storedRowBindingOf). Ledger identity stays the base (name and packagedFlowOwner unchanged). A /meta delete naming the pair is admitted (isPairRowRemoval, runtime-only intent), so removing the override re-arms the base by one registerFlow, never a withdraw. The undeclared remedy and the flow sealed save sentence name the .custom declaration or a new name instead of the clone.",
    "tests": "Red first on main 0b6a52e (production sources restored, the helper module kept, metadata-protocol and service-automation rebuilt): service-automation shadowing/loader-set/sync Tests 14 failed | 41 passed (55); metadata-protocol flow-stored-row-shipped-name + packaged-base-refusal Tests 15 failed | 71 passed (86); objectql protocol-flow-custom-override (new, real ObjectQL engine) Tests 3 failed | 1 passed (4); cloud-connection install gate 1 failed | 6 passed; cli override-declarations 1 failed | 6 passed. Restored with git checkout HEAD -- packages, porcelain empty, rebuilt. Green on main by construction and kept as pins: the five not-admitted rows, ledger off/on, and the N-12 suspended run finishing on the base. At head all pass: service-automation 55/55, metadata-protocol full suite 21954 passed after the two pin updates, objectql 4/4, cloud-connection 7/7, cli 7/7 plus validate-override-declarations 2/2, rest 4/4, runtime 15/15. Ablations via ablation-replace.mjs (metadata-protocol rebuilt in each leg, ablation-dist-preflight exit 0): ABL1 precedence override branch off gives Tests 9 failed | 46 passed, each arming step red on its own (boot pull, kernel:ready, metadata:reloaded, mutation sync); ABL2 view exemption off gives metadata-protocol 5 failed and objectql "by-name (no package): expected SHIPPED to be OVERRIDE"; ABL3 hydration binding off gives the hydration case red and objectql "expected [ undefined, undefined ] to include com.example.showcase.custom"; ABL4 pair-row delete told to the store as override-artifact gives the objectql delete case red. Each leg: ok restored, blob == HEAD, git diff HEAD empty; after all four, rebuild, preflight --absent exit 0 for ABL1 to ABL4, porcelain empty. Pins whose meaning changed, same commit: the flow save sentence (protocol.packaged-base-refusal, sealed-remedy-kind, rest-meta-packaged-flow-refusal, runtime automation-packaged-base-lock: a save names the declaration, a delete still names the clone), register-item-call-sites args (the body is built one line above to record the binding; disposition unchanged), cli override-declarations (flow now admitted; the not-yet-available row moves to action).",
    "gates": "dispatch-gates --commands: 68 families, each run with its exit captured before any pipe, plus the roster gates under touched dirs (check:authz-resolver, check:changeset-fixed, check:error-code-casing, check:route-ledger-census), all exit 0. First pass: check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity exit 3 (unbuilt packages, nothing measured) and check-plugin-teardown-shape --self-test exit 3 (pinned fixture 621a487 missing from the shallow clone); re-run after the type-check-debt closure build and a git fetch of 621a487: all exit 0. --ran: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. typecheck exit 0 for metadata-protocol, service-automation, objectql, cli (retried with a 6 GB heap after one out-of-memory under concurrent gates), rest, runtime, cloud-connection. eslint --no-inline-config over those seven packages exit 0. CI on PR #28693 had started at report time and was not awaited (in_progress).",
    "dogfood_battery": "0e67bba0 · os-verify-lock: VERDICT command-exit 0 · held the lock 1428s · Test Files 369 passed | 3 skipped (372) · Tests 2981 passed | 28 skipped (3009) · no failures (pnpm turbo run build --filter=@objectstack/dogfood, then pnpm turbo run test --filter=@objectstack/dogfood, unsharded)",
    "mcp_calls": "3 — mcp__github__pull_request_read x2 (get #28651 for the body shape, get_check_runs #28693), mcp__github__list_pull_requests x1 (serial check); no write tool",
    "api_writes": "3 — git push of claude/project-thread-jqgfyc; relay fleet-write pr_create (repository_dispatch, run 41314284001, read-back identical, assignees none); this os-dev-report via post-stamped.mjs --comment=28412",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: b · reach: the by-name and layered read envelope of a served flow override still reports deletable: false (it follows the base seal), while the /meta delete naming the pair is now admitted · evidence: servedLockState reads the base item; PR #28693 acceptance notes · dedupe words: flow override deletable envelope Studio · carrier: N-18 #28570 (Studio Customize this flow) owns that surface; noted, not filed",
    "class: c · reach: a package-less stored row beside the pair row of a shipped flow name can take the one bare registry slot, so the boot pull sees it instead of the override until kernel:ready re-arms from the view · evidence: the SchemaRegistry bare slot holds one stored row per name · dedupe words: bare slot package-less flow row override boot pull · carrier: N-16 migration of legacy rows; noted, not filed"
    ]
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

    Landing record · #28412 · PR #28693

    PR #28693 merged by the merge queue as 3ee6af44fd on main (Fixes #28412). The contract review FAIL 6559358939 was followed by triage#1's delta PASS 6561125936 on 56a1fe59, and the seat's ACCEPT is 6561151030. The card closed on the merge.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28693-*' 0 refs
    origin/main by content git diff --quiet 3ee6af44fd origin/main -- the PR's 23 files all 23 identical
    Ancestry git merge-base --is-ancestor 3ee6af44fd origin/main holds

    Release: branch claude/project-thread-jqgfyc merged; nothing unpushed. pm:dispatched removed and the card unassigned.


    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

No one assigned

    Labels

    area:automationdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions