Repository navigation
feat(objectql, plugin-approvals): assignment rules and approval steps read the queue they name from their declaring package (ADR-0150 D3) - #31850
Conversation
… read queue names from their declaring package Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
…that now builds a registry Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d79626ffa3bdd9146400956f97dc23d084786155 && git checkout d79626ffa3bdd9146400956f97dc23d084786155
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a15b544d542fa3eda258732999778b69613618bd 8b657ca81076c90d76e31d14cbee0bf423bfcd19 && git checkout -B drift-repro a15b544d542fa3eda258732999778b69613618bd && git merge --no-ff 8b657ca81076c90d76e31d14cbee0bf423bfcd19
node scripts/docs-audit/affected-docs.mjs --json a15b544d542fa3eda258732999778b69613618bd
|
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic red-line review — #31850 (#31790) at head 0dccea6: CHANGES (two required, both on the approval half). The assignment-rule half: PASS. Your open questions
R1 (required): a step's package comes from the flow body that armed, not from the loader's set. With 0dccea6, an administrator who edits P's approval step to name their own environment-authored queue is refused Fix:
Card B #31696 rekeys flows by (package, name), and this obligation also goes on record there. Until B lands, this PR is the first to derive "the package of a flow step", and E5's last PR (connector_action connectorId by flow package) will copy whatever reading lands here. Please name the reader so that PR can reuse it. R2 (required): a refusal is not a failed read.
Deviations noted, both fine: plugin-approvals vitest Re-ping the epic seat with the new head once R1 and R2 are in. |
… queue from the package of the flow body that runs AutomationEngine.armedFlowPackage(name) answers the package whose own body is armed under a flow name: the shipping package for the loader's body, nothing for a declared .custom override or any other environment-authored body. The automation plugin hands registerFlow the precedence's verdict at every arming step, and the precedence recognizes the override the protocol's view serves in the package's slot by its own overrides declaration. The approval service reads that package instead of packagedFlowOwner, so an override's step resolves environment-wide, as before. A refused queue name now reads as a refusal with its remedy, not as a failed read, and is not retryable. Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
…the precedence resolveFlowPrecedence again asks the loader's set nothing for a name with one contender; the plugin classifies that winner with describeArmedFlowContender when it hands registerFlow its armedFrom. Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
…-gylh1m # Conflicts: # packages/services/service-automation/src/plugin.ts
…kage reader to its shared home The reader of the package whose flow body runs is being built once, for every caller, in its own change. Until it lands, the approval step reads packagedFlowOwner as before; the engine, the precedence and the plugin are back to main. The refusal wording on the approval node stays. Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
…age of the flow body that runs The step's package is now the automation engine's armedFlowPackage: the shipping package for its own body, none for an environment-authored override of it. So an override's step, its fallback slate and the decide-time tally read their queue names environment-wide, as they did before package-scoped reads, instead of from the package the override customizes. Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf Epic red-line review of #31850 (#31790) at head 00fb75d: PASS. It lands once CI is green and the seat ACCEPTs.
Red lines 1–3 hold. Owed: the skills-docs card (Part of #31527, Blocked-by #31790) is the seat's to file, if it isn't filed already. |
|
domain:server#3 · os-steve · session_01ADHkmQXLfvyB5R53VDm3Ka 席位核对 · ACCEPT · #31850 (Fixes #31790) · 2026-11-09T10:13ZHead-sha:
|
…-gylh1m Claude-Session: https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-steve · session_01ADHkmQXLfvyB5R53VDm3Ka 席位核对 · ACCEPT (re-stated on a new head) · #31850 (Fixes #31790) · 2026-11-09T10:51ZHead-sha:
|
…gnment rule or approval step names (objectstack-ai#31947) <!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb15qRXgCty32TE8iQFKHTgK)_ Fixes objectstack-ai#31853 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:自动化技能文档补上「软件包里的分配规则、流程审批节点引用的队列名」怎么解析:先找本包的队列,再找它声明依赖(加 `sys`)里唯一的持有者;否则拒绝。分配规则被拒时记录写成无主、不进队列,服务器日志告警一次;审批节点被拒时不开启(`APPROVER_DIRECTORY_UNREADABLE`,不可重试)。组织自建的规则、不属于任何包的规则和流程,照旧按整个环境查。另加一条 eval 用例,要求声明对队列所在包的依赖。 - 为什么改:PR objectstack-ai#31850 已让这两处按声明它的软件包解析队列,但技能文档没说,AI 会写出能通过校验、运行时却把记录全部写成无主或让审批节点打不开的规则。 - 风险与回滚:只改已发布的技能文本、一条 eval 和两个文件的 token 上限(SKILL.md 6568→6687,参考文档 5825→5916,均为先删重复后的实测余额)。回滚即还原本 PR。 - 先看:`skills/objectstack-automation/SKILL.md` 的「Assignment rules」末段;`references/state-machines-and-approvals.md` Approver Types 表下的新段落。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期两个文件分别 6687 / 5916 tokens、headroom 0、退出码 0。 - 需要你做的:审阅并批准(Tier H,`skills/**`)。 ## What changes - `skills/objectstack-automation/SKILL.md` § Assignment rules: a new paragraph teaches how a packaged rule's `assignTo: { type: 'queue', value }` resolves, the refusals, what the rule does on one, the callers that still read environment-wide, and the organization `sys_queue` fallback; one example (`acme.helpdesk` naming `acme.crm`'s queue declares the dependency). - `skills/objectstack-automation/references/state-machines-and-approvals.md`: the Approver Types `queue` row points to one paragraph under the table, which teaches the same resolution for a packaged flow's `queue` approver and what the node does on a refusal; one example (`acme.deals` naming `acme.crm`'s `crm_finance`). - `skills/objectstack-automation/evals/flows-triggers-approvals.json`: case id 21. The expected output declares `dependencies: { 'acme.crm': … }` on `acme.helpdesk`; `must_contain` needs `defineAssignmentRule`, `type: 'queue'`, `value: 'crm_support_intake'`, `dependencies` and `'acme.crm'`; `must_not_contain` rejects `dependencies: {}` and the package-qualified names `acme.crm/crm_support_intake` / `acme.crm:crm_support_intake`, so the undeclared form fails. - `scripts/check-skills-token-ratchet.mjs`: the two files' rows, pinned at the measurement. - `.changeset/31853-automation-packaged-queue-resolution.md`: `'@objectstack/skills': patch`. Where each taught fact comes from (merged code on `main` `2b880f12`): - Resolution order and reason codes: `packages/core/src/package-scoped-resolution.ts` `resolvePackageScopedHolders` (L599–600 own, L602–613 closure, L615–620 `outside-closure` with the environment item among the candidates; L546–566 the caller with no package, environment item first, else `ambiguous-in-environment`); `PackageScopedRefusalReason` L313–319. - Assignment rule: `packages/objectql/src/assignment-rules.ts` L192 (`_packageId` is the rule's package), L236 (an organization's row is no package), L516–521 (`callerPackageId` passed only when there is a package), L527–529 (the "does not resolve … from the rule's package" warning, `warnOnce` keyed per rule, queue and organization, row written ownerless, in no queue). A refused name answers `null` in `packages/plugins/plugin-sharing/src/ownership-queue-service.ts` L275–282; a name no declared queue holds falls through to the organization's authored `sys_queue` row, L300–301. A disabled package is no holder: `package-scoped-declarations.ts` `listedHoldersView` L145. - Approval: `packages/plugins/plugin-approvals/src/approval-service.ts` L3661–3665 (`flowPackageOf` reads `armedFlowPackage`: none for a deployment-authored flow or a `.custom` override), L3642–3648 (`expandMembers` under `onFailedRead: 'refuse'` with `callerPackageId`), L1349–1359 (`APPROVER_DIRECTORY_UNREADABLE: … because its name was refused (ADR-0150 D3, <reason>)`, the refusal's sentence naming every candidate, the slate, `fallbackApprovers` and administrators all skipped, `retryable` false). - Tests: `packages/objectql/src/ownership-queue.test.ts` L474–620 and `packages/plugins/plugin-approvals/src/approval-service.test.ts` L4423–4622 (no request row written on a refusal). - Card paraphrase vs code: the card says a queue "only the environment holds" is refused. In the code that is a queue declared in the environment's own metadata, with no package; an organization's authored `sys_queue` row of a name no `queues` entry declares still resolves for a packaged rule, so the skill says both. The card's three refusals hold; the code adds that a no-package caller is refused `ambiguous-in-environment` only when two packages declare the name and the environment does not. ### Token budget Standing ruling, verbatim and untranslated: 「A:先削重复,余额按实测提高」 (ruling record 6513306068, letter B). Cuts admitted by claim amendment 6741201566. - `SKILL.md` (priced bytes, stamp line excluded): 26263 -> 26746. The new text adds 695 B, four restatement cuts remove 212 B, so the ceiling moves 6568 -> ceil(26746 / 4) = 6687 (+119; headroom was 2 tokens). - `references/state-machines-and-approvals.md`: 23237 -> 23661. The new text adds 690 B, four cuts remove 266 B, so the ceiling moves 5825 -> ceil(23661 / 4) = 5916 (+91; headroom was 15 tokens). The cuts, each with its survivor: 1. SKILL.md L45, the `api` flow-type row's "— needs a start-node `secretRef`" (-35 B): § Inbound webhook triggers, the `secretRef` / `secret` row (L366), which the row's kept pointer reaches. 2. SKILL.md L104, the `approval` node row's "— suspends the run until a decision, then continues down the `approve` / `reject` branch" (-95 B): the reference's § Approvals (Flow Nodes) opening paragraph (L61–64). 3. SKILL.md L328, the `approvers: [{ type: 'queue', value: 'crm_support_intake' }]` literal, now "a `queue` approver" (-43 B): the reference's Approver Types `queue` row, which the kept link reaches. 4. SKILL.md L387–388, "This is the start node alone, not a whole flow" (-39 B): the section heading ("Trigger Configuration — on the `start` node"). 5. Reference L48, "e.g. `previous.status != 'escalated' && record.status == 'escalated'`" (-71 B): SKILL.md § Trigger Configuration's start-node example. 6. Reference L68–69, "— approval flows live in your normal `flows: [...]`" (-56 B): § Approvals (Flow Nodes), "authored as an Approval node on an ordinary flow". 7. Reference L171, "— you opt in, edge by edge" (-29 B): the same item's "This is the only thing that legalizes the cycle". 8. Reference L272–274, "(flow conditions: the trigger snapshot; hook conditions: the stored record overlaid with the write's payload)" (-110 B): the time-word cheat sheet's flow-condition row, and objectstack-data `references/data-hooks.md` L483 for hook conditions (the file already points there). No pointer was cut, and the state-route line pinned by `packages/rest/src/meta-state-route-doc-spelling.test.ts` is untouched. Bundle readings: `skills/objectstack-automation/SKILL.md` 447 -> 456 lines, 26283 -> 26766 B; all published `SKILL.md` files 4596 -> 4605 lines. ## Tests Docs and eval only; no package code changes. `node scripts/check-skills-token-ratchet.mjs` exit 0 and `--self-test` exit 0. ## Verification <details><summary>Derived gates (dispatch-gates --commands, 65 families)</summary> - Full sweep at `5a47df55`: 65 derived, 65 run, every recorded exit 0; `dispatch-gates --ran`: `65 run, 0 NOT-MEASURED`. Run after a full `turbo run build` (VERDICT command-exit 0), so `check:dual-build-cjs-loads` and `check:doc-formula-expressions` measured, not exit 3. - After merging `main` (6 commits, none touching these files) at `00b7d809`: the derivation is byte-identical; re-run on the new head: `check-skills-token-ratchet`, `check:skill-docs`, `check-empty-changeset`, `check-changeset-no-major`, all exit 0. - PR-body gates on this body: `check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity`, `check-partof-closing-keyword`, all exit 0. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTKa8eJCpKq9hPcYPT)_ Co-authored-by: Claude <noreply@anthropic.com>
Requested by Steve · project thread
Fixes #31790
Clause-②: yes (widening); narrowing per ADR-0150 D3 declared below
Metadata-protocol: none
Skills-docs: follow-up owed, filed as #31853 (domain:skills, blocked by #31790) — skills/objectstack-automation/SKILL.md (Assignment rules: a declared rule's queue is read from its package, else a declared dependency's) and skills/objectstack-automation/references/state-machines-and-approvals.md (Approver Types: a
queueapprover likewise, from the flow's package). The sharing-rule half is already taught in skills/objectstack-data/rules/security.md.Widening (ADR-0150 D3): a declared assignment rule, or an approval step in a packaged flow, in package P naming a queue that two packages declare. Before (main
fdfcc7ab): refusedambiguous-in-environment. After: served when one holder is P's own or the only one in P's declared dependencies — P depends on D, D and unrelated U both hold it (D's queue); P holds it, U too (P's own). First-party: 0 (no first-party package declares a queue, an assignment rule or aqueueapprover).Narrowing (ADR-0150 D3): a declared assignment rule, or an approval step in a packaged flow, in package P whose queue name P cannot reach. Before (main
fdfcc7ab): served environment-wide. After: refusedoutside-closure, naming every candidate. Three cases, measured on the registered queue service (table below): the only holder is a package outside P's dependencies; P's dependency D holds it but is disabled and an unrelated U holds it too (refused naming U); the only holder is an item authored in the environment. An assignment rule then writes the row ownerless, in no queue, and warns; an approval node refuses to open (APPROVER_DIRECTORY_UNREADABLE, the refusal as its cause). First-party: 0 (no first-party package declares a queue, an assignment rule or aqueueapprover).Environment-authored overrides (the epic review's R1, 6734421619): an approval step reads its package from the flow body the engine runs,
AutomationEngine.armedFlowPackage(#31863). An environment-authored.customoverride of a packaged flow therefore reads its queue names with no package, environment-wide, exactly as on main: neither a widening nor a narrowing for it.维护者速读
.custom覆盖:审批步骤按引擎实际运行的那份定义取包(armedFlowPackage,来自 feat(service-automation): a connector_action names its connector from the calling flow's package #31863),覆盖的步骤按「无包」查,与 main 一致(史诗评审 R1)。packages/objectql/src/assignment-rules.ts的route(fromPackage两处调用);packages/plugins/plugin-approvals/src/approval-service.ts的flowPackageOf与expandQueueUsers。pnpm --filter @objectstack/plugin-approvals exec vitest run src/approval-service.test.ts -t "ADR-0150 D3",预期 7 个用例全绿。What changes
@objectstack/objectql,assignment-rules.ts). A declared rule now carries the package its registry item is registered under (_packageId, the registry's stamp; the existing pinthe two stack keys reach the registry with their packageholds it).routepasses it ascallerPackageIdtoresolveQueueandexpandMembers. An organization'ssys_assignment_rulerow is explicitly no package, and a declared rule no package ships carries none: both make exactly today's call, with no options argument. The "does not resolve" warning names the rule's package when there is one. Module doc gains a "Which queue a name means" section.@objectstack/plugin-approvals,approval-service.ts). A step's package is the package of the flow body the engine runs:flowPackageOf(flowName)asks the automation engine's publicarmedFlowPackage(feat(service-automation): a connector_action names its connector from the calling flow's package #31863), the reader the engine's own subflow addressing (ADR-0150 E1) takes a run's caller package from. It answers the shipping package for the loader's own body, andundefinedfor an environment-authored.customoverride (ADR-0150 D3: a reference in an overlay is the environment's) and for a flow authored in the deployment.ApprovalResumeSurfacedeclares that optional method; without it every step reads environment-wide as before. ⛔ NotpackagedFlowOwner, which names the package that ships the NAME whichever body is armed (the first push read it; the epic review's R1).openNodeRequestcomputes the package once and passes it on the node's slate and on thefallbackslate;decide's pre-snapshot re-resolution reads it for the row'sflow:process name, assendBackalready does. It travels throughexpandApproversandresolveApproverSpectoexpandQueueUsers, which addscallerPackageIdbesidepurpose: 'addressing'andonFailedRead: 'refuse'. A deployment-authored flow and an override make today's call unchanged.ApproverDirectoryUnreadableError(APPROVER_DIRECTORY_UNREADABLE), the refusal as its cause, and still routes past no one. When the cause is a D3 refusal (read by its shape,refusal.status === 'refused': this package does not depend on plugin-sharing, which throwsPackageScopedNameRefusedError), the message says the name was refused (ADR-0150 D3, <reason>), carries the refusal's sentence, and ends with its remedy: from package P, declare a dependency on the package whose queue it means, enable that package if it is disabled, or ship the queue in P; with no package, leave one holder of the name, or name the queue from a step in a package that depends on its holder. It no longer says "Run the flow again once the directory can be read", and itsretryableisfalse. A failed read keeps its wording, byte for byte, andretryable: true. No new code: reworded under the existing one (epic review R2).IOwnershipQueueService, implemented by plugin-sharing over objectql'sresolvePackageScopedName) decides.@objectstack/objectqland@objectstack/plugin-approvals, with both declaration lines and the narrowing.Step 0: how each reader learns its declaring package
Measured on
origin/mainfdfcc7ab(with #31789).assignmentRulesitem,host.listRules()=registry.listItems('assignment_rule')_packageId(stamped byregisterItem/applyProtection; already pinned inownership-queue.test.ts)sys_assignment_rulerowqueueapprover)openNodeRequestgetsflowNamefrom the engine-seeded$flowNameAutomationEngine.armedFlowPackage(flowName)(#31863: the package whose own body is armed;undefinedfor an override or a deployment-authored flow)decidepre-snapshot tallyprocess_name(flow:NAME) toarmedFlowPackageThe definition's own
_packageIdis deliberately not read for flows: the engine documents it as the caller's bytes, and an override's served copy carries the base's. Both readers have a coordinate, so no stop.Tests
objectql, new top-level
describein the D5 harnesssrc/ownership-queue.test.ts(5 cases); plugin-approvals, new top-leveldescribeinsrc/approval-service.test.ts(7 cases). Each fixture installs real packages into a realSchemaRegistry(P depends on D; U unrelated; D and U declare queuesupport). The queue service is a double that stands in for plugin-sharing's (which neither package can import: plugin-sharing depends on objectql): it delegates to the one resolver,resolvePackageScopedName, over the holders the listing serves (a disabled package's left out, as plugin-sharing'slistedHoldersViewdoes), and answers a refusal as the contract says (null/[], or underrefusea rejection carrying the refusal).support, held by D and U, reaches D's queueoutside-closure, naming U, U never servedarmedFlowPackageundefined,packagedFlowOwnerP) naming an environment-held queue P cannot reach: served environment-wide, on the node slate and the pre-snapshot tallypackagedFlowOwner(refusedoutside-closure)fallbackslatepackagedFlowOwner"Red on main" was measured by restoring the two source files to
fdfcc7ab(git restore --source), running the blocks, and restoring (git diff HEADempty): objectql 3 of 5 red, plugin-approvals 4 of 5 red, every CONTROL green.Forced test-infrastructure change:
packages/plugins/plugin-approvals/vitest.config.tsdeclaresenv: { OS_REGISTRY_LOG: 'warn' }, the textcheck:registry-log-declaredprescribes, because this suite now constructs aSchemaRegistry. No existing test was edited.Acceptance notes
Clause-②: yes (widening), as feat(spec, plugin-sharing): ownership-queue reads take the caller package, so a declared rule's queue recipient resolves by ADR-0150 D3 #31789 declared the same movement (seat ruling; claim amended on the card, 6734711604), with the narrowing stated beside it. Accepted by the epic review (6734421619).armedFlowPackage, one reader. feat(service-automation): a connector_action names its connector from the calling flow's package #31863 added it for every caller (E1's subflow addressing, E5's connector_action, this step). An R1 first cut built a reader here before feat(service-automation): a connector_action names its connector from the calling flow's package #31863 existed; it was withdrawn on the seat's instruction (fe45524b) and never reached main. This PR only reads feat(service-automation): a connector_action names its connector from the calling flow's package #31863's.nobodyreader by contract, so a refused name answersnulland the queue service reports the refusal once, naming every candidate; the seam says the name did not resolve from the rule's package.AssignmentQueueServiceslice and plugin-approvals'ApproverQueueSourceare unchanged: both arePicks of the contract, which already carries the option.Verification
Head
00fb75d5. It mergesorigin/mainat5d17aa6b, which includes #31863'sarmedFlowPackage, through merge commitf3864c02. The first push was0dccea6c.930d242f,48b425dcandfe45524bcarry an R1 first cut and its withdrawal. Net, they leavepackages/services/**identical to main.00fb75d5switches the approval step to feat(service-automation): a connector_action names its connector from the calling flow's package #31863's reader.pnpm --filter @objectstack/plugin-approvals test(under the lock, on00fb75d5): Test Files 115 passed (115), Tests 1509 passed (1509). The new block is 7/7.Override pins, written first and run on
f3864c02plus the new tests with the reader stillpackagedFlowOwner: 2 red. After the switch: green.pnpm --filter @objectstack/objectql test(under the lock, onfd340a92; this PR's objectql files have not changed since): Test Files 579 passed (579), Tests 11868 passed (11868).typecheck: plugin-approvals exit 0 on00fb75d5(check:test-typecheck: 8 files / 321 errors / 27 signatures held, unchanged). objectql exit 0 at the first push.Lint: neither package has a
lintscript. Rootnpx eslint --no-inline-config packages/objectql/src packages/plugins/plugin-approvals/srcexits 0 on00fb75d5.dispatch-gates --ranon00fb75d5, on a clean tree: 74 derived, 74 run, every one recordedexit 0, 0 NOT-MEASURED, 0 UNRUN.Dogfood, full battery on
00fb75d5under the lock (owed for the(narrowing)arm): Test Files 441 passed, 3 skipped (444); Tests 3662 passed, 42 skipped (3704); VERDICT command-exit 0.Measured: what the registered queue service answers, environment-wide (main's readers) and from P (this PR's)
A throwaway test (deleted, never committed) drove
@objectstack/plugin-sharing'sOwnershipQueueServiceover a realSchemaRegistryon this tree.resolveQueue('support'), andexpandMembersunderrefusefrom P:ambiguous-in-closure[D, D2]outside-closure[U]outside-closure[U]outside-closure[environment]sys_queuerowThe three bold rows are the narrowing; rows 1 and 6 the widening the card asks for.
Ablations: six legs, each red, each restored (scripts/ablation-replace.mjs)
Both suites import their package's source directly; no dist leg. A1–A4 ran on
0dccea6c; R2 on the head below. Each leg: anchor 1 to 0, blob changed, run red, restored with blob == HEAD andgit diff HEADempty.A1, objectql, the rule's package dropped (
fromPackagealways undefined): 3 red, the D-resolve pin, the two-in-closure pin (vocabularyambiguous-in-environment, no package in the warning) and the disabled-D pin (U's member served).A2, objectql, a refused name falling back to the environment-wide read: 2 red, the disabled-D pin (U's queue served) and the two-in-closure pin (a second refusal). The first attempt was a no-op the tool refused (the replacement still held the anchor, count 1 to 1); re-run with an anchor the replacement does not contain.
A3, plugin-approvals, the flow's package dropped: 4 red, every package pin.
A4, plugin-approvals, a refused name answered as its first registered candidate (the rejection caught and re-asked from
refusal.candidates[0]): 3 red, the two-in-closure pin (D's member served), the disabled-D pin (U's member served) and the CONTROL (the environment-wide refusal served D).R1, plugin-approvals, the step's package read from
packagedFlowOwner(the first push's reader), on00fb75d5: 2 red, both override pins. The pins were also written first and run red against that reader before the switch.R2, plugin-approvals, the refusal read as a failed read (
packageScopedRefusalOfansweredundefined): 2 red, the two-in-closure pin (failed-read wording,retryable: true) and the CONTROL.The objectql reader never sees the candidates (the pool is a
nobodyreader, a refusal readsnull), so its "first registered" leg is A2, the one fallback that reader could write.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmPfjQpqnk93rr9esh8