Repository navigation
fix(runtime)!: a job name a package body declares and the residual carries is refused, never pulled as the body - #32410
Conversation
…ries is refused, never attributed to the body (#32355) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
…poseStacks artifact, with its controls (#32355) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
…sidual-job-package Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
… label (#32355) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
…sidual-job-package Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
…sidual-job-package
…which keys the pair under one name (#32355) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
…-twin pin (#32355) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
…sidual-job-package
…sidual-job-package
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 29 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 b71c175d2be956cfcffa5dfcea8837762eb94773 && git checkout b71c175d2be956cfcffa5dfcea8837762eb94773
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b96d492db8641c7d8b16ee9f95babec879ceafac bd92690d4e38d3ee2df33005003f1f3edbd7210c && git checkout -B drift-repro b96d492db8641c7d8b16ee9f95babec879ceafac && git merge --no-ff bd92690d4e38d3ee2df33005003f1f3edbd7210c
node scripts/docs-audit/affected-docs.mjs --json b96d492db8641c7d8b16ee9f95babec879ceafac |
|
domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w Epic red-line read of #31527 for #32410 (#32355, the residual-twin job refusal), at head Verdict: the red lines hold, but no PASS yet. One must-fix is owed before landing: the head predates #32405, and one of this PR's pins will go red on the merged tree. One should-fix is carried as a card, and there is one nit. I read the net diff against 1. ADR-0150 D3 and D4: agrees
2. Siblings: clean
3. Order: clear
Owed items
Recorded for the ADR (Tier H, not this PR)D3 says a job reads from "the job's package". It does not say what a top-level job outside every body is. The next amendment card should state: the residual's job runs as the body that carries the artifact's id, or as no package, and a name it shares with a body's job is refused (#32071, #32355). Generated by Claude Code |
|
domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w Rendered 2026-11-11T10:13Z by a contract-review subagent of the seat, narrowing face, read-only. Inputs: card #32355 (body, every comment, dev report 6792591092), this PR's body, file list and net diff against Contract reviewServed-tier: ① Derived judgmentsRead in the tree at the head, not in the PR body.
② Semver levelMatches. The diff moves ③ Boundary flags
④ Design judgmentThe narrowing makes the spec's own refusal real; it takes no declared capability away at the consumer. ADR-0150 D3 gives a bare name two outcomes, one holder or a refusal that lists the candidates, and says of every by-name read that it guesses nothing. Attributing a top-level job to a body because the body declares a job of the same name is a guess: the residual registers under Is "recognise a residual twin by count" sound? Tried to break it against the cited code:
Findings. No must-fix, no should-fix. Three nits; none touches correctness or the contract.
Implemented-by: VERDICT: PASS Generated by Claude Code |
…sidual-job-package
…ual trigger, and a disabled twin on either side still refuses the name (#32355) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
|
domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w Epic red-line re-read of #31527 for #32410 at head Verdict: PASS on the red lines. M1 is closed, and nothing is owed on this PR. I read the delta
At this reading the head's checks stand at 35 passed and 2 in progress, and the PR is still a draft. Generated by Claude Code |
|
domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w Delta review rendered 2026-11-11T11:47Z by the same contract-review subagent, narrowing face, read-only. It follows record 6793121403 (PASS at Contract reviewServed-tier: Check-runs on this head, read at render time: 36 success, 18 skipped, 0 failed, 1 in progress. The one in progress is a second ① Derived judgmentsHeld, on the merged tree. The PR's own source hunks against
② Semver levelUnchanged and still matching: ③ Boundary flags
④ Design judgmentUnchanged. The narrowing still makes ADR-0150 D3's refusal real and takes no declared capability away at the consumer; the count logic in Findings on the delta. No must-fix, no should-fix. One nit.
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w 席位核对ACCEPT · head
|
|
domain:server#7 · os-justin · session_01QSeKYm8sWjp2Pk76warN7w Landing · head
|
| Probe | Reading |
|---|---|
check-runs on bd92690d (latest run per check) |
55: 37 success · 18 skipped · 0 failed |
| review · governed · base | contract delta PASS 6794975177 and epic re-read PASS 6794968868 on this head; dogfood battery VERDICT exit 0 on this head; NOT governed, 263 lines ≤ 3000; K = 1; merge-tree clean |
State: ready + auto-merge.
Fixes #32355
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no published skill teaches a job
pull, or a manifest-less input to acomposeStacks(…, { manifest: 'preserve' })composition维护者速读
callerPackageId)去拉取、读那个包的映射,这是"凭名字猜"的归属,违反 ADR-0150 D3。pull)。回滚即 revert 本 PR;唯一受影响的写法是同名 job 横跨包体与顶层残余,改名即可。packages/runtime/src/app-artifact-handlers.ts的jobDeclaringPackages(按计数识别残余同名,residualCarries分支)与jobNameRefusal的残余措辞;再看测试文件末尾的[#32355]describe。pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/app-artifact-handlers.job-pull.test.ts -t 32355,预期 5 个用例全绿。What changes
FROM → TO. FROM: in an artifact loaded from JSON, a top-level job that no package body declares (the ADR-0130 residual, emitted by
composeStacks(…, { manifest: 'preserve' })for an input with nomanifest) and that shares its name with exactly one body's job was matched to that body by name, so its pull ran as that body's package (callerPackageId) and read that package's mappings. TO: such a name answers no package; each run is refused before any call or write,409 RESOURCE_CONFLICT(ambiguous-in-environment), naming the declaring body and the residual, reusing the two-body refusal shape. Exception, unchanged: when the residual's own package (the body that carries the artifact's id) IS the one declaring body, the name answers that package. Fix for an author who hits it: rename the top-level job, or the body's, so each job name has one declaring holder.On a JSON-loaded artifact the body's own job of that name is refused too, including where it used to pull correctly as its body: nothing in the artifact tells the body's flattened copy from the residual's job.
jobDeclaringPackagesrecognises the residual twin by COUNT: the binder's jobs hold more jobs of the name than the bodies declare (the flattened top level copies each body job once). Never by comparing a copy's content with its body's.jobNameRefusaltakes an optional residual holder and names it, as its owning package when a body carries the artifact's id (then also a candidate), else as the top level that names no package.jobDeclaringPackagesanswers.Step 0: can the shape boot? Yes, measured
os buildon a real config composing a module body with asyncpull job, a manifest-less stack with its ownsyncpull job, and an app body: exit 0,✓ Build complete. The artifact's top-leveljobsis[["sync","alpha_map"],["sync","host_map"]], the bodies[["com.example.alpha",["sync"]],["com.example.beta",[]]],manifest.idcom.example.beta. Nothing in the build refuses the pair: the same-stack duplicate check (collectDuplicateEntryErrors) runs per input stack and does not read the composition. The probe config was temporary and is not in the diff.jobKeyFor), and every shipped adapter replaces on a duplicate key (CronJobAdapter.scheduleandIntervalJobAdapter.storecancel first). So the key does not prevent the shape. It collapses the pair to the LATER job in top-level order, which on the build's own order is the residual's.judgeJobPullbinds (the residual's mapping is at the top level), and jobs are not registered as metadata items (TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL), so no registry refuses the pair first.CronJobAdapterrecords the surviving run pulling{ mapping: 'host_map', callerPackageId: 'com.example.alpha' }: the residual's mapping, pulled as the body that declares the name.Census: 0 first-party pairs. The one first-party
preservecomposition (examples/app-multi-package) gives every input a manifest and declares no jobs, and no first-party job declares apull.Tests
packages/runtime/src/app-artifact-handlers.job-pull.test.ts, a top-leveldescribein the existing job-pull harness file. The runs go through a realCronJobAdapter(@objectstack/service-job), so which job of the pair survives is the job service's answer, not the test's:composeStacksemits, through JSON, carries the pair (the body's copy and the residual's own job).409 RESOURCE_CONFLICTnaming both packages.jobDeclaringPackagesanswers the refusal for every job of the name, and names a residual that has no package.com.example.alpha.trigger: 'manual'thatCronJobAdapter.trigger()hands and the binder forwards, and nojobRunId, because that adapter writes no run row.enabled: false) twin on either side still refuses the name. Every job of the name answers the refusal, the enabled one is scheduled, and its run fails 409 before any pull.Ablation, once, on
38ef518bc2. The anchorif (residualCarries.has(name as string)) {was replaced withif (false && …)throughscripts/ablation-replace.mjs(anchor 1 → 0, blob6d43531fdb28→74e26a44a08b). Pins 2 and 4 went red, 2 failed | 2 passed. Pin 2's diff shows the pre-fix pull ofhost_mapascom.example.alpha. After the restore (blob == HEAD,git diff HEADempty) the four pass. The source is imported relatively fromsrc, so nodist/is involved.Acceptance notes
resolveArtifactCollectionslets the top-level name claim the body's job, and the count sees one job of the name. That reader behaviour is not changed here.Verification
The PR head is
bd92690d4e. It mergesorigin/mainthree times over the work commits, the last time at5467af4325, which brought in #32405 (the jobrunId/triggerforwarding), plugin-security, spec and service-analytics. Every reading below is onbd92690d4e. After it,mainmoved again by one commit, which was not merged in: the merge queue tests the merged state.Runtime suite and typecheck, under the shared verify lock
Dogfood battery, under the lock, the same command CI runs (no shard, no path)
Lint and derived gate families
eslint --no-inline-configon the two changed.tsfiles: exit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 65 families, run onbd92690d4e. 63 exited 0. 2 exited 3 (PREREQUISITE NOT MET, so NOT MEASURED locally, and CI'sLint & Repo Gatesruns both):check:dual-build-cjs-loadsneeds a fullpnpm build, andcheck:type-check-debtnever got the verify lock to re-measure.--ranreconciliation: 65 derived, 65 run, 0 UNRUN..changeset/32355-residual-job-twin-refused.md,@objectstack/runtime: patch, BREAKING banner, both declaration lines, ADR-0087 dispositionnot-required (no-migration-prescription).check-adr-0087-registration,check-changeset-no-majorandcheck-empty-changesetall exited 0.Generated by Claude Code