Repository navigation
spec(integration): build the connector sync executor that syncConfig and fieldMappings declare (14 keys), once and on the mainstream shape #20281
Description
Activity
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsPath: approvals and automation | 缺项 (no item authors a connector
syncConfig/fieldMappings) | P2Triage: first grade —
enhancement·priority:p3·domain:spec·pm:queue. Verdict: ENFORCE, by the maintainer's criterionTriage: the parent lands in
packages/spec/src/integration/connector.zod.tsand the ledger. The consumer is new: a sync executor (inservice-automationor a new service) that readssyncConfig+fieldMappingsfrom a materialized declarative connector, schedules it and writes through ObjectQL ⇒domain:specparent, with adomain:servicessub-issue the spec seat files at dispatch (ruling A′ ②). Rationale: 14 keys parse and publish. Their defaults (conflictResolution: latest_wins,deleteMode: soft_delete) read like configured policy, and nothing applies them. No measured author, and a new executor is a large build off the road ⇒ p3.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T20:30Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), the criterion on #18900 (5727134555), and the #20273 / #20274 / #20282 grades that applied it this week.Verdict. Declarative external sync is mainstream: ServiceNow Import Sets with transform maps, Power Platform dataflows with incremental refresh, and Salesforce Data Cloud data streams. By the criterion ⇒ ENFORCE, 「补消费端(一次做对)」. It is not a decision-box round-trip. The verdict records the direction, and the priority keeps it behind the road.
Execution notes.
- Build it once, on the mainstream shape: field maps, an upsert key, scheduled full or incremental runs on a watermark, and the declared conflict and delete policies applied, each pinned.
- Until it ships,
SYNC_ARCHITECTURE.mdstops presenting the capability as delivered (a one-line honesty fix that rides the first PR). - ⛔ Stop valve: if the executor needs a credential or scheduling contract the spec does not declare, stop and report here; do not invent one.
- Not folded with spec(integration): retire the connector health-probe, circuit-breaker, authored
statusand nestedwebhookskeys (16), which nothing enforces #20273 (RETIRE, the same schema file). The first to claim goes first, and the other mergesmain.
- addedenhancementNew feature or requestNew feature or request
on Sep 27, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-decision-facets
决策请求:连接器数据同步做成什么样?定义放在目标表一侧(复用现有的导入映射),还是留在连接器上补齐,还是不做? · 2026-09-30T03:51Z
domain:specseat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1)。分诊5859569880已按你的标准定为 ENFORCE(做)。但它的第 3 条执行说明是停止阀:「执行器需要 spec 没有声明的凭证或调度约定时,停下来报告,不许自己发明」。本席的只读普查(本地c-20281/census.md,基于origin/main0d9349fe)测得停止阀在动手之前就已触发,所以把方向放进决策箱。本卡同时转入needs-user-decision。一句话问题
连接器上声明的 14 个同步配置今天没有任何程序执行。要让它真的能用,缺的不只是执行器:目标表、数据从哪读、按什么匹配、多久跑一次,这些在协议里都没有位置。所以得先定同步定义放在哪。
Governing text
- 本卡分诊
5859569880:ENFORCE,「按主流形状一次做对」;第 3 条停止阀(原文见上)。 - 本卡正文:主流平台(ServiceNow 导入集 + 转换映射、Power Platform 数据流、Salesforce Data Cloud 数据流)都把同步定义挂在目标表或数据流上,从不挂在连接上;「一次做对」包括决定形状是否还留在连接器上。
packages/spec/src/integration/connector.zod.ts:295-306(维护者 2026-09-10「直接删」裁决时写下):syncConfig.schedule在 spec 17 被直接删除,没有墓碑,也没有 D2/D3;定时同步应当是一个job(Job.schedule.expression,平台唯一会执行的定时位),由它的处理函数驱动连接器。- 协议声明:选项 A、C 都会让连接器上的键离开已发布的
strictObject(破坏性,要走 ADR-0087);B 会重新加回 09-10 删掉的定时键。
前提(每条带复查方法)
- 14 个键没有任何读取者。 复查:
git grep -n -P '\bsyncConfig\b|\bfieldMappings\b' origin/main -- packages examples apps ':!packages/spec/**',本席读数只有 2 处,都是packages/qa/dogfood/test/expression-conformance.ledger.ts:457,459的注释。没有示例应用写这两个键(普查)。 - 协议里缺的东西(普查 §1、§3、§4): 目标对象、数据源的读取方式(列表、分页、水位)、匹配键、执行节奏、执行者所属组织、水位存在哪。运行时注册连接器时会丢掉
syncConfig和fieldMappings(普查:service-automation/src/plugin.ts:1674)。 - 目标一侧已经有一半是活的。
mapping元数据类型(packages/spec/src/data/mapping.zod.ts:310-317)已有目标对象、字段映射、写入模式(新建/更新/新建或更新)和匹配键,台账 7 个键全部live。REST 导入器runImport已经按匹配键做「有则更新、无则新建」,并拒绝匹配到多条的行(普查 §4.3)。 - 有的默认值根本没有运行时。
deleteMode默认soft_delete(connector.zod.ts:331-335),但平台没有软删除,所有删除都是硬删除(data/object.zod.ts:1507-1511)。
选项与代价
选项 做什么 客户看到的后果 A 定义放在目标一侧:扩展现有的 mapping类型,加「数据来自哪个连接器」。写入复用runImport。节奏按 09-10 裁决的方向,由job驱动,不加回定时键。连接器上的 14 个键按 ADR-0087 迁移或退役。第一版只做单向拉取(外部 → 本地),全量或按时间戳增量与主流一致:业务管理员在「导入映射」里多一个「定期从连接器拉取」。双向同步、删除同步、人工冲突处理第一版没有 B 留在连接器上补齐:给 syncConfig加目标对象、数据源、匹配键,并加回定时键形状不动;但推翻 09-10 的删除裁决。一个连接器只能写一个目标,与导入映射各自一套 C 不做声明式同步:14 个键全部退役;需要同步的应用写一个 job处理函数,调现有导入器平台少一项能力,应用要自己写几十行代码;协议最简单。等于把分诊的 ENFORCE 改成 RETIRE 业务直译:A = 「导入」和「定期同步」是同一件事,只是后者按时自动跑;B = 同步挂在连接上,与导入分开维护;C = 平台不管同步,谁要谁自己写。
四轴(业务立场)
- 长远合理性: 两年后的样子:同步是目标表上的一份映射,按时从外部系统拉取,与手动导入共用一套匹配和校验规则。主流参照见本卡正文(本席未实测)。A 复用已活的
mapping与runImport,不新造第二套映射,特例最少。B 让导入映射和连接器同步各有一套,还要推翻 09-10 的裁决。 - 实际业务拉动: 今天没有任何示例应用或已知客户写这 14 个键(普查);分诊定的是 p3。拉动只影响排期,不改变方向。
- 防 AI 犯错: 今天这 14 个键是静默的:
soft_delete、latest_wins这些默认值读起来像已配置的策略,实际什么都不做。A 和 C 都会让它们退出连接器,再写就被响亮地拒绝;B 要一个个补上运行时,否则仍然静默。 - 创业阶段不扩散: A 在一个已有类型上加一个数据源绑定,不新建类型;C 最省;B 要维护两套映射。
推荐
A。 只看①选 A;②③④ 是否翻转:否(②只把它留在 p3,晚些排)。回退:C。
置信缺口:没有测过真实客户要同步哪些系统;runImport在@objectstack/rest里,执行器从服务层调用它的依赖方向还没核实;第一版只支持哪类连接器,要等设计阶段量。裁后执行(维护者只裁方向)
- A: 本席按普查的 9 个分叉定执行参数,并在本卡逐条写明;你可在回复里推翻任何一条。
- 凭证:复用 ADR-0097 的静态凭证,不加新约定。
- 数据源:第一版只支持
rest/openapi连接器。 - 写入:复用
runImport。 - 执行器放在已持有连接器注册表的
service-automation。 - 没有运行时的策略值:改默认值或收窄枚举。
- 离开连接器的键:墓碑加 D2 转换。
- 分阶段:spec(
mapping加数据源绑定 + 连接器键退役)→ 服务层执行器 →job驱动。每阶段一张 PR、达档复核、落地。
- B: 本席按「补齐连接器」立阶段;加回定时键前,先在本卡记下它推翻了 09-10 的裁决。
- C: 本卡转为退役:14 个键走 ADR-0087,改正
SYNC_ARCHITECTURE.md。 - 无论哪项,执行器上线前,
SYNC_ARCHITECTURE.md不再把这项能力写成已交付(分诊第 2 条)。
四棱
① 项目长远合理性:A 与主流一致,复用已活的映射与导入器,不造第二套;B 增加特例。
② 实际业务拉动:已测作者 0,p3;只影响排期。
③ 防 AI 犯错:今天 14 个键静默;A、C 让它们响亮退出,B 要逐个补运行时。
④ 创业阶段不扩散:A 在现有类型上加一个绑定;C 最省;B 维护两套。
Prior rulings read: syncConfig, fieldMappings, sync executor, data sync → 本卡正文与分诊、2026-09-10「直接删」裁决(connector.zod.ts:295-306,#16320)、L1 #4738 与 L2 #6414 的退役(普查 §5);ADR: ADR-0049(enforce-or-remove)、ADR-0087(迁移)、ADR-0097(连接器凭证);thread: 本卡 1 条评论。
推荐:A。只看①选 A;②③④ 是否翻转:否。置信缺口:见上。
Generated by Claude Code
- 本卡分诊
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsRuling: batch #251 item 1 · letter A · maintainer 「同意」 2026-09-30T05:38Z
Director seat (objectstack#12708, summon #30 续 2,
session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「同意」 to batch #251 as presented (this card as item 1, with #18386 and #18164); 「同意」 is to the director's recommendation in chat, which adopted the seat's decision request5903692970(recommendation A) after this seat's own reading of the census facts:packages/spec/src/data/mapping.zod.tsalready declarestargetObject,fieldMapping,mode(insert / update / upsert) andupsertKey, all live;connector.zod.ts:295-306records the 2026-09-10 deletion ofsyncConfig.schedule("sync on a cadence is ajob");runImportlives inpackages/rest/src/import-runner.ts; triage5859569880ruled ENFORCE with the stop valve this request tripped.Ruled: A. The sync definition lives on the TARGET side: the existing
mappingmetadata type gains a source binding that names the connector it pulls from; writes reuserunImport's upsert-by-match-key; cadence is ajob(Job.schedule.expression), per the 2026-09-10 ruling — no schedule key returns to the connector. The 14 connector keys (syncConfig.*,fieldMappings.*) leave the connector under ADR-0087 (tombstone + D2 conversion + D3 entry). Version 1 is one-way pull (external → local), full or timestamp-incremental. ⛔ Not B (two mapping systems, and it reverses 2026-09-10). ⛔ Not C (the mainstream has the capability; triage's ENFORCE stands).Readings recorded with the ruling:
- Long-run: one mapping model for a manual import and a scheduled sync; the shape ServiceNow Import Sets, Power Platform dataflows and Data Cloud data streams share — the definition bound to the target, never to the connection.
- Pull: zero measured authors today;
priority:p3stays — this ruling fixes the direction, not the schedule. - AI-safety:
soft_delete/latest_winsstop reading as configured policy; a connector-sidesyncConfigwritten after this is refused loudly with the prescription. - Startup scope: one binding on an existing type; no new metadata kind.
Execution parameters (ruled in the same stroke — the seat's nine forks, each a parameter the maintainer may overturn on this card):
- Credentials: ADR-0097 static credentials; no new contract.
- Source: v1 supports
rest/openapiconnectors only. - Write:
runImport. The executor lives inservice-automation(it holds the connector registry). The dependency direction (service-automation→ the import runner in@objectstack/rest) is measured before stage ②; if it inverts a layer, the runner's core moves to a shared package — never a copy — and the seat states which on this card before claiming stage ②. - Policy values with no runtime (
deleteMode: soft_delete,conflictResolution): narrow the enum or change the default, so nothing reads as configured that does nothing. - Keys leaving the connector:
retiredKey()tombstones, one D2 conversion, one D3 entry — in the spec stage. - Stages, one PR each, at-tier review each, landed in order: ① spec —
mappinggains the source binding and the 14 connector keys retire (Clause-②: yes, BREAKING banner, ADR-0087 disposition), andSYNC_ARCHITECTURE.mdstops presenting the capability as delivered in the same PR; ② service — the executor; ③ thejobdriving it. - Not folded with spec(integration): retire the connector health-probe, circuit-breaker, authored
statusand nestedwebhookskeys (16), which nothing enforces #20273 (RETIRE, the same schema file): the first to claim goes first; the other mergesmain. - Confidence gaps recorded: real customer sync targets unmeasured; the
runImportdependency direction unmeasured; v1 connector kinds set at design.
State:
needs-user-decision→pm:queue(domain:spec,priority:p3stay); the Ruled line goes on the body in the same stroke. Thedomain:specseat that filed the request writes these parameters into the card's dispatch and files the stage cards as it claims.- added and removed
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 24 (stage ① of ruling A
5904845660: the spec stage) · 2026-09-30T14:11Z
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20281-mapping-source-binding
Worktree:objectstack-issue-20281-s1
Domain:domain:spec
Seat:domain:spec#5(seat post #19357)
Clause-②: yes (narrowing)
Scope: stage ① exactly as the ruling's execution parameters set it. The 14 connector keys leave the publishedstrictObject, which is a narrowing. The newmappingbinding widens the accept set.- The source binding.
mappinggains a binding that names the connector it pulls from,rest/openapiconnectors only in v1. It is a one-way pull, either full or timestamp-incremental.- ⛔ No schedule key: a
jobdrives the cadence, per the ruling of 2026-09-10. - ⛔ No new credential contract: ADR-0097 static credentials apply.
- The dev proposes the minimal shape, grounded in the census (
5903692970§6 (b)): the connector reference, the read operation, and an optional watermark field. It lands asplannedrows withauthorWarn, because stage ② builds the reader.
- ⛔ No schedule key: a
- The 14 keys retire.
syncConfig.*andfieldMappings.*leaveconnectorthroughretiredKey()tombstones whose prescription points at themappingsource binding and ajob, one D2 conversion, and one D3 entry. The policy values with no runtime (deleteMode: soft_delete,conflictResolution) leave with them. packages/spec/docs/SYNC_ARCHITECTURE.mdstops presenting connector sync as delivered, in the same PR.
File surface (stop on breach and explain it in the report):packages/spec/src/data/mapping.zod.ts,packages/spec/src/integration/connector.zod.ts, and their tests.packages/spec/src/migrations/**(the tombstones, the D3 entry and the regenerated registry) andpackages/spec/src/conversions/**(the D2 conversion and the regenerated registry).packages/spec/liveness/{connector,mapping}.jsonand their state counts.packages/spec/docs/SYNC_ARCHITECTURE.md.- Generated projections, regenerated only. Every pin that counts or enumerates what moves.
.changeset/20281-*.md:@objectstack/specminorwith a BREAKING header and the ADR-0087 disposition marker.- ⛔ No executor and no
jobwiring: those are stages ② and ③, and the seat files their cards as it claims them.
Container & model:M/L,mode:subagent,model: opus.
Review: an at-tier contract review is owed before enqueue.
Thread-read: 5904845660
Serial constraints: read at this stamp, PR fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level options is refused by name (stage iv of #20051) #20868 (spec(ui)+metadata save: judge a flattened view overlay's top-leveloptions.KINDwith the strict per-kind schema and persist the PARSED body — the door half of objectui#10380 #20051 stage iv, another seat) touchespackages/spec/src/migrations/registry.ts, and no open PR touchesmapping.zod.ts,connector.zod.tsorSYNC_ARCHITECTURE.md. ⛔ The dev writes this PR'smigrations/registry.tsandconversions/registry.tshunks only after merging amainthat contains fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level options is refused by name (stage iv of #20051) #20868's landing. Until then those hunks wait and everything else proceeds. If fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level options is refused by name (stage iv of #20051) #20868 is still open when the rest is done, the dev stops and reports, and the PR stays draft.
Generated by Claude Code
- The source binding.
32 remaining items
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (stage ③ of ruling A
5904845660, built to the maintainer's ruling5974483403: Q1-B + Q2-O1) · 2026-10-03T23:58Z
Session:session_01T9u38rswFp5Rw8DswRUReJ
Account:os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20281-stage3-job-pull
Worktree:objectstack-issue-20281-s3
Domain:domain:spec
Seat:domain:spec#1(seat post #6017)
File surface (atorigin/main15fe567c9c; stop on breach and explain in the report):- spec:
packages/spec/src/system/job.zod.ts, which gains thepull: { mapping }run form (exclusive withbody/handler; writing two is refused) andorganization. Also the pull method onpackages/spec/src/contracts/automation-service.ts, and their tests, liveness row (packages/spec/liveness/job.json), generated artefacts and docs. - runtime: the binding branch in
packages/runtime/src/app-artifact-handlers.tsthat binds a pull job and carries the job's organization (body, handler and pull alike), plus its tests. - service-automation: the pull method on the automation service (
packages/services/service-automation/src/plugin.ts,connector-pull.ts), plus tests. - lint /
os validate: the mapping-name check and the posture rule for a job'sorganization, reusing the scheduled-flow rule of the 2026-09-08 ruling. - The stale texts the phase-0 report named:
packages/spec/src/data/mapping.zod.ts(about:430) and its test,connector.zod.tsSYNC_CONFIG_RETIRED,packages/spec/docs/SYNC_ARCHITECTURE.md(about:97–:99),connector-pull.ts(about:30) andplugin.ts(about:639). - ⛔ Not
packages/spec/src/data/hook.zod.ts:domain:engine's [Decision] security(objectql): may a hook'shandlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604 holds itshandlerdoc (declaration5974628902). ⛔ No sandbox capability (Q1-A is not taken). ⛔ No second organization spelling (Q2-O3 is not taken).
Container & model:L,mode:subagent,model: opus; the at-tier contract review is a separate isolated subagent after delivery.
Clause-②: yes (widening)
Thread-read: 5974483403
Serial constraints cleared: PR fix(runtime,cloud-connection)!: install-local refuses a hook with no body and a job body that does not bind, and withholds such a hook on rehydrate (#21585) #21615 ([finding] os package install accepts a package whose hook uses only the deprecated function-name handler (no body), answers "installed", and the hook never fires: install-local drops it with a server-side warn only #21585), which this stage waited on for the binder file, landed as045b946256. No open PR touches any path above (scan at this stamp). Open PR feat(spec)!: retire the object-master-detail-form detail entry sortField — the console derives the line-position field from the child object (#21589) #21632 (this seat) touchescomponent.zod.tsand the migration registries, which this surface does not.
- spec:
- added a commit that references this issue
on Oct 4, 2026 objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20281, "stage": "3 (the job driving the pull), built to ruling 5974483403 (Q1-B + Q2-O1)", "status": "done", "branch": "claude/issue-20281-stage3-job-pull", "pr": "https://github.com/objectstack-ai/objectstack/pull/21668", "session": "session_01T9u38rswFp5Rw8DswRUReJ (the dispatching PM session; this run is its subagent)", "premise_still_valid": true, "summary": "Q1-B: JobSchema gains the closed run form `pull: { mapping }`, refused beside `body` or `handler` (at-least-one now spans body/handler/pull; body+handler unchanged). IAutomationService gains `pullConnectorSource` (+ ConnectorSourcePullRequest/Result/Summary), served by the AutomationEngine from the executor AutomationServicePlugin attaches at init (`setConnectorPullSource`). The ONE binder (`scheduleAppArtifactJobs`) schedules a pull job on every door and calls the contract method through the service registry on each run (refused pull = failed/retried, refused rows = degraded, else completed). `defineStack` (so `os validate`) refuses a pull naming an undeclared mapping or one with no connectorSource. Q2-O1: JobSchema gains `organization` (ScheduleOrganizationSchema reused); the binder judges it at bind with `resolveScheduledWorkPolicy` (isolated + switch on: required, else not scheduled at error; group: optional, undeclared named at warn; single: not required), and every form runs as `{ isSystem: true, tenantId: ORG }` (body ctx.api envelope, pull context, handler executionContext). The texts that said \"nothing schedules a pull yet\" are corrected. Behaviour change to read: on an isolated deployment with package scheduled work ON, a packaged job declaring no organization is now not scheduled (it was scheduled and its tenant-scoped writes refused); the changeset states the action.", "mechanism_assumptions": { "1_posture_rule_reuse": "CONFIRMED with a boundary: the predicate resolveScheduledWorkPolicy (packages/types/src/env.ts) and the value shape ScheduleOrganizationSchema are reused, not copied. The refusal sentence describeMissingScheduleOrganization is flow-shaped (start node config), so the job has its own sentence (describeMissingJobOrganization in app-artifact-handlers.ts) - a sentence, not a second rule. No fork.", "2_pull_reachable_only_on_plugin": "CONFIRMED. The contract method lands on the engine (the registered automation service) via setConnectorPullSource; the binder resolves ctx.getService(automation) at bind and again on every run. Pinned: connector-pull-service-door.test.ts (booted LiteKernel service reaches the plugin executor) and connector-pull.integration.test.ts (second pull through the service, real rest connector + SQLite).", "3_binder_install_local": "PARTLY DISPROVED. Unchanged, collectJobsWithoutBody would have named every pull job as body-less, so os package install would REFUSE every pull job with the wrong prescription; now it never names one. Measured through the real install-local door with a deleted probe (runtime dist at a3e9317f77): a pull package installs 200, is scheduled, and its run pulls under {isSystem:true, tenantId:org_a}; a pull job whose mapping the package lacks installs 200 and is NOT scheduled (binder warn names pull.mapping). The door does NOT refuse that second case: a refusal needs a cloud-connection describeUnrunnable clause, outside the claimed file surface -> open question 1.", "4_liveness_row": "CONFIRMED. liveness/job.json gains pull (drilled: mapping live + producer) and organization (live + producer); gen:liveness-counts moves job to 21 live / 23 classified." }, "dispatch_conflicts": [ "The dispatch said \"the same exactly-one rule the job already applies to body / handler\". The job applies AT-LEAST-ONE: body + handler is legal and body wins. Built per the ruling text: pull excludes both; the old pair unchanged (narrowing it would be breaking).", "The dispatch said the posture check is \"a rule inside existing validators (parse / os validate)\". The posture and the scheduled-work switch are environment facts, unknowable at authoring; schedule-organization.zod.ts places the scheduled-flow rule at bind and forbids an authoring-time lint for it, and the ruling says to use the rule scheduled flows use. Built at bind; os validate checks the mapping name only." ], "tests": "All test/build runs via scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 unless stated). spec full suite `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` at 36da2bfc88: 610 files, 18075 passed, 1 todo (the first run at 1b0a4b3d0f had 1 failure, alias-integrity: three alias pairs collapsing onto one probe; fixed in 36da2bfc88, re-run green). spec typecheck at 36da2bfc88: green incl. test layer. runtime `vitest run --project local --maxWorkers=2` at 1b0a4b3d0f: 319 files, 4550 passed, 19 skipped. service-automation `vitest run --maxWorkers=2` at 1b0a4b3d0f: 169 files, 2081 passed. runtime + service-automation typecheck: green incl. test layers (36da2bfc88 changed only job.zod.ts aliases and one spec test after that). New suites: spec system/job-pull-organization.test.ts (18), runtime app-artifact-handlers.job-pull.test.ts (20), service-automation connector-pull-service-door.test.ts (3). cloud-connection marketplace-install-local-jobs.test.ts against the new runtime dist: 11 passed. Ablations, all through node scripts/ablation-replace.mjs WRAP mode on committed code (anchor hit proven on disk; restore proven blob == HEAD and git diff HEAD empty; subjects imported from src, no dist in the path): A drop collectJobPullMappingErrors call -> 4 failed/14 passed; B exclusivity predicate always true -> 2 failed/16 passed; C requiresActingOrganization branch unreachable -> 2 failed/18 passed; D buildJobSandboxContext never carries tenantId -> 1 failed/19 passed; E pullRunOutcomeOf never degraded -> 1 failed/19 passed; F drop the collectJobsWithoutBody pull skip -> 1 failed/19 passed. Cross-package reverse verification: temporary runtime probe typing {mapping, bogusKey} as ConnectorSourcePullRequest -> tsc TS2353 on that line, while the IAutomationService[pullConnectorSource] line compiled (the rebuilt spec .d.ts was read); probe deleted. Door readings (deleted probes): os validate on a config pulling orders_pul -> exit 1, code STACK_CROSS_REFERENCE_INVALID; the corrected name loads past defineStack (its exit 1 is only docs/namespace-required + docs/metadata-embed-ref from the probe location, no cross-reference error). Lint (proven narrowing, CI owns pnpm lint): population from eslint.config.mjs = **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED; eslint --no-inline-config --format json over the 23 changed lintable files at 36da2bfc88 = 23 files, 0 errors, 0 warnings; invariance: the config enables no type-aware linting (its own statement) and this diff edits neither the config nor a file it reads. NOT MEASURED, declared for CI: packages/cli integration tier (package-install-local-jobs.integration.test.ts) - no CLI file or spawn entry touched.", "gates": { "node scripts/check-adr-0087-registration.mjs --base origin/main": 0, "node scripts/check-adr-0087-registration.mjs --self-test": 0, "node scripts/check-changeset-no-major.mjs --base origin/main": 0, "node scripts/check-changeset-no-major.mjs --self-test": 0, "node scripts/check-ci-filter-parity.mjs": 0, "node scripts/check-closing-keyword-parity.mjs": 0, "node scripts/check-closing-keyword-parity.mjs --self-test": 0, "node scripts/check-comment-mask-adoption.mjs": 0, "node scripts/check-comment-mask-adoption.mjs --self-test": 0, "node scripts/check-comment-mask-corpus.mjs": 0, "node scripts/check-dev-prereqs.mjs --self-test": 0, "node scripts/check-doc-frontmatter.mjs": 0, "node scripts/check-doc-frontmatter.mjs --self-test": 0, "node scripts/check-doc-route-spelling.mjs --advisory": 0, "node scripts/check-doc-route-spelling.mjs --self-test": 0, "node scripts/check-docs-section-name.mjs": 0, "node scripts/check-docs-section-name.mjs --self-test": 0, "node scripts/check-dts-emitted.mjs --self-test": 0, "node scripts/check-empty-changeset.mjs --base origin/main": 0, "node scripts/check-empty-changeset.mjs --self-test": 0, "node scripts/check-issue-citations.mjs": 0, "node scripts/check-keyed-text-bounds.mjs": 0, "node scripts/check-keyed-text-bounds.mjs --self-test": 0, "node scripts/check-platform-object-tenancy-census.mjs": 0, "node scripts/check-platform-object-tenancy-census.mjs --self-test": 0, "node scripts/check-plugin-teardown-shape.mjs": 0, "node scripts/check-plugin-teardown-shape.mjs --self-test": 0, "node scripts/check-registry-log-declared.mjs": 0, "node scripts/check-registry-log-declared.mjs --self-test": 0, "node scripts/check-rest-log-spy-declared.mjs": 0, "node scripts/check-rest-log-spy-declared.mjs --self-test": 0, "node scripts/check-section-landing-index.mjs": 0, "node scripts/check-section-landing-index.mjs --self-test": 0, "node scripts/check-spec-docblock-symbol-anchors.mjs": 0, "node scripts/check-spec-docblock-symbol-anchors.mjs --self-test": 0, "node scripts/check-system-context-census.mjs": 0, "node scripts/check-system-context-census.mjs --self-test": 0, "node scripts/check-tenant-audit-census.mjs": 0, "node scripts/check-tenant-audit-census.mjs --self-test": 0, "node scripts/check-undeclared-dep-imports.mjs": 0, "node scripts/check-undeclared-dep-imports.mjs --self-test": 0, "node scripts/docs-audit/check-affected-docs.mjs": 0, "node scripts/docs-audit/check-drift-comment.mjs": 0, "node scripts/pm/release-rehearsal-clone.mjs --self-test": 0, "node scripts/release-pending-publish.mjs --self-test": 0, "pnpm --filter @objectstack/lint run check:doc-formula-expressions": 0, "pnpm --filter @objectstack/lint run check:doc-security-posture": 0, "pnpm --filter @objectstack/spec run check:api-surface": 0, "pnpm --filter @objectstack/spec run check:authorable-surface": 0, "pnpm --filter @objectstack/spec run check:browser-reachable-entries": 0, "pnpm --filter @objectstack/spec run check:docs": 0, "pnpm --filter @objectstack/spec run check:dual-source-exports": 0, "pnpm --filter @objectstack/spec run check:duration-unit-keys": 0, "pnpm --filter @objectstack/spec run check:empty-state": 0, "pnpm --filter @objectstack/spec run check:entry-nameability": 0, "pnpm --filter @objectstack/spec run check:export-origins": 0, "pnpm --filter @objectstack/spec run check:exported-any": 0, "pnpm --filter @objectstack/spec run check:generated": 0, "pnpm --filter @objectstack/spec run check:liveness": 0, "pnpm --filter @objectstack/spec run check:llms-txt": 0, "pnpm --filter @objectstack/spec run check:migration-registry": 0, "pnpm --filter @objectstack/spec run check:objectui-pin-citations": 0, "pnpm --filter @objectstack/spec run check:skill-examples": 0, "pnpm --filter @objectstack/spec run check:skill-refs": 0, "pnpm --filter @objectstack/spec run check:spec-changes": 0, "pnpm --filter @objectstack/spec run check:strictness-ledger": 0, "pnpm --filter @objectstack/spec run check:upgrade-guide": 0, "pnpm --filter @objectstack/spec run check:variant-docs": 0, "pnpm --filter @objectstack/spec run check:yaml-examples": 0, "pnpm check:changeset-gate-self-tests": 0, "pnpm check:corpus-claim-drift": 0, "pnpm check:cross-package-test-inputs": 0, "pnpm check:dispatcher-error-vocabulary": 0, "pnpm check:doc-anchors": 0, "pnpm check:doc-authoring": 0, "pnpm check:docs-audit-scope": 0, "pnpm check:docs-redirects": 0, "pnpm check:docs-single-h1": 0, "pnpm check:docs-spec-enumerations": 0, "pnpm check:docs-transcript-drift": 0, "pnpm check:driver-memory-census": 0, "pnpm check:dts-closure": 0, "pnpm check:dual-build-cjs-loads": 0, "pnpm check:engine-double-contract": 0, "pnpm check:future-spec-major": 0, "pnpm check:gitlink-declared": 0, "pnpm check:issue-citations": 0, "pnpm check:lean-entry-closure": 0, "pnpm check:logger-receiver-detach": 0, "pnpm check:merge-driver": 0, "pnpm check:nul-bytes": 0, "pnpm check:objectql-double-limit": 0, "pnpm check:objectui-changeset": 0, "pnpm check:org-identifier": 0, "pnpm check:page-declaration-shape": 0, "pnpm check:platform-checklist": 0, "pnpm check:pm-changeset-deadline-census": 0, "pnpm check:pm-prior-rulings": 0, "pnpm check:pm-widening-tells": 0, "pnpm check:published-files": 0, "pnpm check:published-readme-links": 0, "pnpm check:query-options-erasure": 0, "pnpm check:quick-reference-counts": 0, "pnpm check:react-page-adapter-contract": 0, "pnpm check:refd-timer-probe": 0, "pnpm check:role-word": 0, "pnpm check:skill-identifier-liveness": 0, "pnpm check:skill-top-level-keys": 0, "pnpm check:slot-lookup": 0, "pnpm check:sourcemap-no-sources-content": 0, "pnpm check:spec-parsed-alias": 0, "pnpm check:stack-collection-maps": 0, "pnpm check:test-source-alias": 0, "pnpm check:tier-file-adoption": 0, "pnpm check:type-check-coverage": 0, "pnpm check:type-check-debt": 0, "pnpm check:vendor-version-stamps": 0, "pnpm check:watch-hint-literal": 0, "pnpm check:where-matcher": 0, "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-final.list": 0 }, "gates_note": "Final union at 36da2bfc88: 119 derived, 119 run, all exit 0; --ran reconciliation \"119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN\" (exit 0). The first union at a3e9317f77 found one real drift (check-system-context-census [declared-count] 23 vs 25, the two new isSystem type declarations; fixed by pnpm gen:system-context-census -> content/docs/permissions/system-context.mdx) and two exit-3 prerequisites (check:skill-examples, check:dual-build-cjs-loads), both green once their builds existed.", "deviations": [ "File-surface extensions beyond the claim list, each required by the ruling or by a derived gate: packages/runtime/src/sandbox/body-runner.ts (the body ctx.api envelope carries the organization) and packages/runtime/src/job-handler-context.ts (handler executionContext) - Q2-O1 \"body, handler and pull all run as that organization\"; packages/spec/src/stack.zod.ts (the mapping-name check lives in defineStack validateCrossReferences, the existing validator os validate reaches via refuseUnbuiltStack, instead of a packages/lint rule); content/docs/automation/jobs.mdx (docs for both keys); content/docs/permissions/system-context.mdx (gate-derived census count); comment-only edits in packages/lint/src/authoring-rules.ts and service-automation/src/index.ts (texts made false); generated artifacts via check:generated --fix; dropped-refinements.baseline.json hand-applied from the build-printed corrections.", "No cloud-connection file was edited: the install-local door clause is reported as open question 1 instead (claim says stop on breach).", "One-off probes created and deleted, never committed: packages/cloud-connection/src/zz-probe-20281-pull.test.ts (install-local door), packages/runtime/src/zz-reverse-20281.ts (type reverse verification), examples/app-showcase/zz-probe-20281.config.ts (os validate). git status clean after each.", "The three-suite run held the lock as one batch joined with \";\" (the lock verdict line names only the last part); each suite exit was recorded separately (SA-EXIT=0, RT-EXIT=0, SPEC-EXIT=1 -> fixed and re-run alone with VERDICT command-exit 0). The batch pid file vanished mid-run (cause unknown, TMPDIR unset); the PID was re-read from ps and waited on - results unaffected.", "No command was refused by the permission classifier. Reads: single-card REST reads only (gh api issue 20281 and its comments; PR 21668 read-back). No search, no board pull." ], "files_changed": [ ".changeset/20281-job-pull-organization.md", "content/docs/automation/jobs.mdx", "content/docs/permissions/system-context.mdx", "content/docs/references/data/mapping.mdx", "content/docs/references/integration/connector.mdx", "content/docs/references/system/job.mdx", "docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md", "packages/lint/src/authoring-rules.ts", "packages/runtime/src/app-artifact-handlers.job-pull.test.ts", "packages/runtime/src/app-artifact-handlers.jobs.test.ts", "packages/runtime/src/app-artifact-handlers.ts", "packages/runtime/src/app-plugin.job-data-reach.test.ts", "packages/runtime/src/job-handler-context.ts", "packages/runtime/src/sandbox/body-runner.ts", "packages/services/service-automation/src/connector-pull-service-door.test.ts", "packages/services/service-automation/src/connector-pull.integration.test.ts", "packages/services/service-automation/src/connector-pull.ts", "packages/services/service-automation/src/engine.ts", "packages/services/service-automation/src/index.ts", "packages/services/service-automation/src/plugin.ts", "packages/spec/api-surface/contracts.json", "packages/spec/authorable-surface/system.json", "packages/spec/docs/SYNC_ARCHITECTURE.md", "packages/spec/dropped-refinements.baseline.json", "packages/spec/export-origins/contracts.json", "packages/spec/liveness/job.json", "packages/spec/liveness/mapping.json", "packages/spec/liveness/state-counts/job.md", "packages/spec/src/contracts/automation-service.ts", "packages/spec/src/data/mapping-connector-source.test.ts", "packages/spec/src/data/mapping.zod.ts", "packages/spec/src/integration/connector-sync-retirement.test.ts", "packages/spec/src/integration/connector.zod.ts", "packages/spec/src/migrations/entries/semantic/18.connector-sync-keys-retired.ts", "packages/spec/src/migrations/registry.ts", "packages/spec/src/stack.zod.ts", "packages/spec/src/system/job-pull-organization.test.ts", "packages/spec/src/system/job.zod.ts" ], "line_budget": { "additions": 1552, "deletions": 107, "source": "`git diff --shortstat 15fe567c9c...HEAD` = 38 files changed, 1552 insertions(+), 107 deletions(-) at 36da2bfc88" }, "clause2": "Clause-②: yes (widening) is at line start in the PR body (line 3) and in .changeset/20281-job-pull-organization.md (@objectstack/spec, @objectstack/runtime, @objectstack/service-automation: minor). No new refusal falls on a key that existed before; the isolated-posture bind refusal is a runtime behaviour change the changeset states.", "mcp_calls": "0", "api_writes": "3 REST writes, each one relay stroke (POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #21668; read-back 15744 bytes sent = 15744 stored, identical); (2) label-write assign -> POST /repos/objectstack-ai/objectstack/issues/21668/assignees (os-project-manager; read-back matches); (3) this report -> POST /repos/objectstack-ai/objectstack/issues/20281/comments (scripts/pm/post-stamped.mjs). Plus git push of claude/issue-20281-stage3-job-pull (not REST). No skip-changeset (the change publishes).", "open_questions": [ { "question": "Should os package install REFUSE a pull job that does not bind (its mapping missing from the package, or without a connectorSource), as it refuses a body job whose body does not bind? Today it installs 200 and the binder warns and does not schedule it.", "options": [ "A. A follow-up card in cloud-connection: UnrunnableCode.jobs gains a pull refusal and describeUnrunnable a pull clause, and collectJobsWithoutBody (runtime) names a non-binding pull job with a pullRefusal read from judgeJobPull. Small (two files plus tests), same shape as the body judgement, closes the installed-200-never-scheduled gap for hand-written JSON packages.", "B. Leave it: defineStack / os validate already refuse the authoring shape, so only a hand-edited JSON package reaches the door, and the binder says why at warn." ], "recommendation": "A, as a separate card. Business need: zero measured pull authors today, so no urgency, but the cost is small. Long-run: the door and the binder judging by the same judgeJobPull mirrors judgeJobBody. AI-safety: a 422 at install beats a warn in a server log (the exact shape the body judgement closed). Startup scope: no new gate, one clause. It was not built here because cloud-connection is outside this claim file surface." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · packages/spec/dropped-refinements.baseline.json measured.refinementSitesThatDidProject reads 369 while the build measures 444 (pre-existing drift; the test pins only the other two header counts). Not in the PR Acceptance notes (the body is written once) - the seat may append it.", "carrier: 承接者:无 · content/docs/permissions/system-context.mdx ungated rows (object-literal / type key 310 vs census 329, and the source totals) are pre-existing drift the census gate does not hold; only the gated declaration row moved here.", "carrier: 承接者:无 · packages/spec/liveness/connector.json syncConfig note still says connectorSource is \"recorded planned in mapping.json until the pull executor reads it\" - stale since stage 2, historical narrative, not made false by this change." ] }Delivered at 2026-10-04T02:02Z on objectstack
36da2bfc88(stage 3, draft PR #21668).
Generated by Claude Code
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsContract review
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:36da2bfc8882a5fbbb9d5277f7e37d8a18587f8b
Local-runs: nonePR #21668 (draft, 38 files, +1552 / −107, no governed path) on card #20281 — stage ③ of ruling A (
5904845660), built to the governing ruling5974483403(Q1-B + Q2-O1). Inputs: the card body and all sixteen comments (rulings and both earlier records included), the PR body, its file list, the net diff againstmainatfea67065a3, and the head's check-runs. Base-tree facts the diff rests on were read at the base sha through the API (job.zod.ts,schedule-organization.zod.ts,types/env.ts,stack.zod.ts,mapping.zod.ts,app-artifact-handlers.ts,cli validate.ts,stack-provenance-refusal.ts,trigger-schedule). Nothing built, run or re-run. The dev report5975609901was read as a claim and tested against the diff.Check-runs on the head, read at 2026-10-04T02:15Z: 27
success, 2skipped(Console Pin Gate and Packed-tarball smoke — path- and label-filtered, as on the stage-① head), 0failure, 5 stillin_progress:Lint & Repo Gates, andTest Coreshards 1, 2, 4 and 5 of 6. No failing step and no error line to name. Already green on the required set:TypeScript Type Check(all four Type Check jobs),Build Core,Dogfood Regression Gate(3/3),Temporal Conformance (live PG + MySQL),Governed Surface Queue Guard; alsoCheck Changeset,Spec property liveness,Check PR Size, and the three claim-parity checks. The seat converges the remaining five; this record judges the diff.① Derived judgments
Each accept-set or public-surface change the diff implies, named right or wrong against the ruling text and the base tree.
JobSchema.pull— a closed{ mapping }withmapping: SnakeCaseIdentifierSchema, optional. Right. Q1-B's third run form, a widening of the authorable accept set. The name pattern is the same schemamapping.nameuses (mapping.zod.ts:332), so no declared mapping name is unreachable frompull.mapping. Closed from day one; an unrecognized key inside it is refused.- The run-form rule. Right, and the dev's reading of the dispatch conflict is the correct one. The base rule is
requiredOneOf(['body', 'handler'])with "when both are presentbodywins" (job.zod.ts:234, :295at base) — AT-LEAST-ONE, not exactly-one. The ruling sayspullis "mutually exclusive withbodyandhandler; writing both is refused". The diff does exactly that:requiredOneOfwidens to the three keys (a pull-only job now parses), and a new refinement refusespullbeside either, located atpull.body+handlerstays legal. Narrowing the old pair would have been a BREAKING change the ruling never ordered. The exclusivity has no arm in the closed projection list, so the published JSON Schema is wider than the parse there; the site is recorded indropped-refinements.baseline.json(660 → 665:system/Jobroot plus the fourmanifest.jobs.elementechoes), and the ledger note onpullsays so. Right. - The alias table. Right.
mapping/sync/connectorSource→pull, andorganizationId/orgId/tenantId/tenant→organization(the probe folds case and underscore, so the snake spellings are caught too); onpullitselfname/mappingName/source/connectorSource/from→mapping. Every one of these keys was already refused on the closed shape; they are now refused with a pointer. The accept set does not move. JobSchema.organization—ScheduleOrganizationSchemaby reference, optional. Right. Q2-O1 with ONE value shape (the test pins parity with the flow schema on'org_a','x','',7,null), no second organization spelling (Q2-O3 not taken — the key is on the job, not onpull). Optional at parse by design: whether it is required is the deployment's posture, which is not knowable at authoring.defineStack→validateCrossReferencesgainscollectJobPullMappingErrors. Right. The ruling's "os validatechecks the mapping name" —os validateloads throughloadConfigandrefuseUnbuiltStack, sodefineStack's own cross-field refusals are the validator it relies on (validate.ts:246).mappingsisz.array(MappingSchema)on the definition (stack.zod.ts:708) and the existing loop at:2917reads it as an array, so the new array read cannot false-refuse a record form. Both halves refused (undeclared name; declared mapping with noconnectorSource), with the existingSTACK_CROSS_REFERENCE_INVALID/ 422 envelope; placed ahead of theobjectNames.size === 0early return, like the global-update-action rule beside it; a disabled job is judged too. Every refusal falls on a key that did not exist before.IAutomationService.pullConnectorSource?plusConnectorSourcePullRequest/ConnectorSourcePullResult/ConnectorSourcePullSummary. Right. An additive, optional contract member (thegetConnectorDescriptorsprecedent, with the same reason stated),context?: ExecutionContexttyped from the kernel shape, three new exports withapi-surface/contracts.jsonandexport-origins/contracts.jsonregenerated. The result type exposes the runner's tallies and not its per-row results, which the integration pin now reflects.AutomationEngine.setConnectorPullSource/pullConnectorSource. Right. The registeredautomationservice is the engine, so the contract method lands there and the plugin hands it the call atinit()beforeregisterService— the materialized-connector map stays the plugin's. A bare engine rejects withSERVICE_UNAVAILABLE503 rather than answering a pull that never ran: the loud refusal, not a silent no-op. The booted-kernel pin and the end-to-end integration pull now go through the service door.- The ONE binder (
scheduleAppArtifactJobs). Right.judgeJobPulljudges a pull against the artifact's resolved collections (ADR-0130 D4, sibling packages included — pinned) and the parsedJobSchema.pull; a pull that does not bind is not scheduled and the reason is said atwarnwith the key named (pull.mapping). Each run resolves theautomationservice again through the registry (pinned) and maps the outcome once (pullRunOutcomeOf): a refused pull rejects →failedandretryPolicy; refused rows →degradedwith the counts; otherwisecompleted, an empty pull included. A composition with no pull door is not scheduled, naming the plugin to compose. Result gainspulls; retirement sets include pulls.collectJobsWithoutBodynever names a pull job — right, because naming it would prescribe abodythe schema refuses besidepull. - The organization, judged at bind. Right, and the second dispatch conflict is correctly resolved in the ruling's favour. The ruling says "the deployment-posture rule that scheduled flows already use"; that rule lives at bind —
schedule-organization.zod.ts:60-63at base: "⛔ do not re-add an authoring-time lint for it: at authoring time neither the switch nor the posture is knowable". The binder importsresolveScheduledWorkPolicyfrom@objectstack/typesand readsrequiresActingOrganizationandrunOwnership === 'per-record'— the very readsschedule-trigger.ts:503, :738make. A reuse, not a copy.describeMissingJobOrganizationis a sentence of its own because the flow's names a start node'sconfig; it is not a second rule. Posture read once per call and only after the switch (with the switch off the posture is never read — pinned). The ruling's own table ③ places parse /os validateon the mapping name and the exclusion, and says only that the organization declaration is checkable — consistent. jobExecutionContext(org)on every run form. Right forbody(buildJobSandboxContexttakes the organization; the real-sandbox pin shows{ isSystem: true, tenantId }on the write) and forpull(the request'scontext). Forhandlerthe envelope is HANDED asJobHandlerContext.executionContextandqlstays the raw engine, pinned as such byapp-plugin.job-data-reach.test.ts(qlIS the engine) and declared in the PR's acceptance notes. Accepted: imposing it would have changed a pinned handle on a deprecated form, and the two non-deprecated forms carry the envelope by construction. A declared organization is applied on every posture, which the describe says.- Behaviour change on an
isolateddeployment. Right, and ruled. With package-authored scheduled work switched ON underisolated, a packaged job declaring noorganizationwas scheduled before and is now NOT scheduled, logged aterrorwith the remedy, counted inmissingOrganization. Before this change such a job ran and every tenant-scoped write it made was refused at the write (walled-posture), so what is lost is a run whose tenant work could not land; a job doing no tenant-scoped work is also withheld, which is the ruling's "required underisolated" (the 2026-09-08 ruling extended to jobs) and the switch is OFF by default in every posture.group: scheduled, named once atwarn(per-record ownership has no record on a job).single: unchanged. Stated in the changeset with the operator action. Right. - Unreadable posture, switch on:
withheld: 'scheduled-work-policy-unreadable', the app's jobs retired, said aterror. Right.resolveTenancyPosturealready refuses an unrecognizedOS_TENANCY_POSTURE("Refusing to boot rather than silently falling back",env.ts:156), so this limb is reachable only where the posture is read late; failing closed matches the resolver's own contract (a typo must not resolve tosingleand drop the requirement), and retiring the app's jobs on a withholding path is what the base binder'sscheduled-work-disabledpath already does (:525at base). Stated in the changeset. - Runtime public surface. Right.
AppArtifactJobSchedulinggainspullsandmissingOrganizationand onewithheldvalue;JobHandlerContext.executionContext?is additive;jobBodyRunnerFactory's job shape gainsorganization?;judgeJobPull,resolveJobOrganization,jobExecutionContext,pullRunOutcomeOfare exported. The result-shape pin and the context-keys pin moved with the change. Thecloud-connectionreader of the result was run against the newdistby the dev (11 passed), not re-run here. - Texts the change made false, corrected. Right and complete against the ruling's list:
mapping.zod.tsTSDoc andconnectorSourcedescribe,connector.zod.tsSYNC_CONFIG_RETIRED(a tombstone prescription — a runtime string, still tracker-free), the D3 entry and the regeneratedmigrations/registry.ts,SYNC_ARCHITECTURE.md(five places, with aJobspecimen beside theMappingone),connector-pull.ts,plugin.ts,service-automation/src/index.ts, thelint/authoring-rules.tscomment, themapping.jsonnote, and the two pins that asserted "nothing schedules … yet", now asserting the absence of that sentence.hook-bodies.mdxuntouched (its planned line belongs to Q1-A) andreleases/untouched — both right. - Liveness and generated artefacts. Right.
liveness/job.jsongainspull(drilled:mappinglive, four evidence anchors, two producers) andorganization(live, evidence on the binder, the envelope builders and the tenancy guard; producerresolveScheduledWorkPolicy);state-counts/job.md19 → 21 live, 21 → 23 classified;Spec property livenessis green on the head.authorable-surface/system.jsongains the two keys; the strictness-ledger count moves by one; the three reference pages andsystem-context.mdx(declared-count 23 → 25, the two new{ isSystem: true; tenantId? }declarations) are regenerated, not hand-written. - Docs. Right.
content/docs/automation/jobs.mdxgains "Pulling a mapping" and "The organization a job runs as" (the posture table matches the binder limb for limb); no new page, so nometa.jsonmove owed.
Nothing in the diff is wrong against the ruling. One thing the diff deliberately does not do is judged under ③ (the install-local door).
② Semver level
- Changeset
20281-job-pull-organization.md:@objectstack/specminor,@objectstack/runtimeminor,@objectstack/service-automationminor. Right. Spec publishes two new optional authorable keys, a widened at-least-one rule, an additive optional contract method and three new exported interfaces — no key, export or accepted input removed. Runtime publishes additive result fields, an additive context member and a ruled bind-time behaviour change behind a default-off switch. Service-automation publishes two additive engine methods.minoris the floorClause-②: yesdemands and the ceiling the diff reaches: nothing authorable narrows, so no BREAKING banner and no ADR-0087 disposition are owed, and none is written. Notskip-changeset— three released packages publish.Check Changesetis green on the head. Clause-②: yes (widening)— right, at line start in the PR body (line 3) and in the changeset body. Every new parse-time ordefineStack-time refusal falls on a key that did not exist before (pull,organization, their near-miss spellings). The one refusal that falls on a pre-existing shape is theisolatedbind refusal of a job with noorganization: a runtime scheduling verdict, not an authoring narrowing, ordered by the ruling and stated with its action in the changeset. The changeset's closing sentence ("Every new refusal falls on a key that did not exist before this change") is parse-scoped by the sentence before it; the seat may tighten it to say "parse-time" so the bind paragraph above cannot be read as contradicted — a wording note, not a defect.- Packages touched without a changeset:
packages/lint(two comment lines, publishes no behaviour);content/docs/**,docs/audits/**and the spec's generated artefacts are not published surfaces. Right. - Size 1,659 changed lines — under the 5,000 human-merge threshold; no governed path, so the queue path applies once CI is green and this record stands.
③ Boundary flags
From the dev report
5975609901, each answered or escalated:dispatch_conflicts1 — "exactly-one" vs the tree's at-least-one. Answered: the dev is right, see ① item 2. The ruling text governs the dispatch paraphrase; the old pair is untouched by design.dispatch_conflicts2 — posture rule at bind, not at parse /os validate. Answered: the dev is right, see ① item 9.os validatechecks the mapping name and the exclusion; the posture rule is the scheduled flows' bind-time rule, reused by import.mechanism_assumptions1–4. Confirmed as stated: predicate and value shape reused, sentence local (1); the pull reached only through the plugin before, now on the service with the booted-kernel pin (2); the install-local door would have refused every pull job with the wrong prescription without thecollectJobsWithoutBodyskip, measured through the real door with a deleted probe (3);job.jsonrows live with evidence (4).deviations— file-surface extensions beyond the claim list. Accepted, each required by the ruling's own text or a derived gate:body-runner.tsandjob-handler-context.ts(Q2-O1: "body, handler and pull all run as that organization" — the claim's "runtime: the binding branch, carrying the organization" cannot reach the body'sctx.apiwithout them);stack.zod.ts(the ruling's "os validatechecks the mapping name", placed in the existing validatoros validatealready relies on rather than a new lint rule — the better placement);jobs.mdx(the ruling names docs);system-context.mdx(gate-derived regeneration); comment-only edits where a sentence became false; generated artefacts regenerated only wherecheck:generatedproved them stale; the dropped-refinement baseline hand-applied from the build's printed corrections. The claim said "stop on breach and explain"; these are explained and none breaches the ruling. Not touchingcloud-connectionwas the right stop. Probes created and deleted, the lock-batch note, and the spec re-run after the alias-integrity fix are accepted as measurement discipline.open_questions1 — shouldos package installrefuse a pull job that does not bind, as it refuses a body-less job? Escalated to the seat with a recommendation: A, as a separate follow-up card, not blocking this landing. The hole is reachable only by a hand-written JSON package —defineStackandos validaterefuse the authoring shape (① item 5) — and the binder says why atwarn, naming the key and the remedy. But the body precedent (an enabled job no door can run is a 422 at install, not a log line) is the shape "absence must be loud" asks for, the remedy is small (acloud-connectiondescribeUnrunnableclause plus the runtime half naming a non-binding pull with its ownpullRefusalfromjudgeJobPull), and it crosses thecloudlane, which is why it did not belong in this PR. The seat files it.out_of_scope_findings. Noted, none made false by this diff: the pre-existingrefinementSitesThatDidProjectdrift in the dropped-refinements header (369 vs a measured 444 — the pin holds the other two counts), the ungated rows ofsystem-context.mdx, and theconnector.jsonsyncConfignote that still narrates the stage-① state. The seat may append them to the PR's acceptance notes or file the first as a card.- Not flagged by the dev, judged here: the
handlerform carries the organization as a handed envelope rather than an imposed one (① item 10) — declared in the PR's acceptance notes, accepted on the grounds given there. The ruling's exclusions hold: nohook.zod.tsedit, no sandbox capability, no second organization spelling, no cadence key on mapping or connector.
Implemented-by:
claude/issue-20281-stage3-job-pull
Reviewed-by:session_01T9u38rswFp5Rw8DswRUReJVERDICT: PASS
Generated by Claude Code
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsACCEPT — PR #21668 at
36da2bfc88(stage ③, built to ruling5974483403: Q1-B + Q2-O1)domain:specseat 1 ·os-project-manager· sessionsession_01T9u38rswFp5Rw8DswRUReJ· 2026-10-04T02:18Z · the review of record for the stage-③ report on this card; contract review PASS5975694925(at-tier, isolated, on this head).Checklist (read on GitHub, not from the report):
-
Form: draft, base
main, first lineFixes #20281, the ruling's landing line. It is the body's only closing keyword.Clause-②: yes (widening)stands in the body and the changeset (@objectstack/spec,@objectstack/runtimeand@objectstack/service-automation, allminor). -
Scope: 38 files, +1552 / −107. Not governed (0 of 38 paths hit the register). The claim's surface is extended by the paths the report lists, each required by the ruling or a derived gate:
body-runner.tsandjob-handler-context.ts: body, handler and pull all run as the organization;stack.zod.ts: the mapping-name check sits in the validatoros validatereaches;jobs.mdxand the census-derivedsystem-context.mdx;- comment-only corrections.
The record accepts each one.
hook.zod.ts([Decision] security(objectql): may a hook'shandlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604's) is untouched. -
Contract review: owed (path leg and
Clause-②: yes) and on record. PASS5975694925on36da2bfc88,Served-tier: CONTRACT_REVIEW_TIER,Local-runs: none.
The two dispatch conflicts, answered for the build:
- Run-form exclusivity. The job's existing rule is at-least-one of
body/handler, not exactly-one (job.zod.ts:295, verified by the record).pullexcludes both, and the old pair is unchanged; narrowing it would be breaking. That satisfies the ruling's "mutually exclusive withbodyandhandler". The dispatch's "exactly-one" was the seat's misstatement. - Where the organization rule runs. The rule runs at bind, reusing
resolveScheduledWorkPolicy(the same readsschedule-trigger.tsmakes) andScheduleOrganizationSchema, not a copy. The posture and the scheduled-work switch are deployment facts, andschedule-organization.zod.tsforbids an authoring-time lint for them. The ruling says to use the rule scheduled flows use.os validatechecks the mapping name.
The behaviour change, accepted as ruled: on an
isolateddeployment with package scheduled work on, a packaged job that declares noorganizationis no longer scheduled. Before, it was scheduled and its tenant-scoped writes were refused. The changeset states the action. The record's wording note: the changeset's closing sentence ("every new refusal falls on a key that did not exist before") is parse-scoped. It is read beside the stated bind refusal, not as contradicting it, and it does not warrant a push of its own.Open question 1 (install-local does not refuse a pull job that cannot bind; it answers
200and the job is never scheduled): A, filed as #21672. The reach is measured at the public install door (the dev's probe). It is the same defect class #21585 closed for a body that does not bind. The authoring doors already refuse the shape, so only a hand-edited JSON package reaches the install door with it. The fix crosses intopackages/cloud-connection, outside this claim, so it is not built here.Out-of-scope findings, noted, not filed (no carrier): the pre-existing
refinementSitesThatDidProjectheader drift (369 vs 444), the ungatedsystem-context.mdxrows, and the stale narrative inliveness/connector.json'ssyncConfignote. None is made false by this change.Landing: CI on
36da2bfc88reads 31 success, 2 skipped andTest Core (1/6)in progress. The seat flips it ready and arms auto-merge once every check is green.⚠️ migrations/registry.ts(one entry text) is also touched by #21464 stage 5 and #20749 stage 8, in flight. Whichever lands later mergesmain.-
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsLanded: stage ③, PR #21668 →
909229e976. Ruling A's three stages are completedomain:specseat 1 ·os-project-manager· sessionsession_01T9u38rswFp5Rw8DswRUReJ· 2026-10-04T03:16Z · holder of claim5974797810.- Landed: PR feat(spec,runtime,service-automation): a job pulls a mapping by declaration (pull: { mapping }) and runs as its declared organization (#20281 stage 3) #21668 merged through the merge queue at 2026-10-04T03:16Z as
909229e976. It has one parent (417443eb27) and is an ancestor oforigin/main. - Content check against the reviewed head
36da2bfc88(contract review PASS5975694925, ACCEPT5975717250):- 36 of 38 files are blob-equal at
909229e976. - The other two were also touched on
mainafter the PR's merge base15fe567c9c. Inmigrations/registry.ts, feat(spec)!: retire the object-master-detail-form detail entry sortField — the console derives the line-position field from the child object (#21589) #21632 added its entries. Inservice-automation/src/engine.ts, fix(service-automation): flow write nodes refuse a stored-metadata family target (#21624) #21649 added its flow write-node refusal. The queue merged both. - Measured by change lines: this PR's changes on each file (
15fe567c9c → 36da2bfc88) equal the merge's (417443eb27 → 909229e976), andmain's changes (15fe567c9c → 417443eb27) equal what the merge kept beside them (36da2bfc88 → 909229e976). Both intents stand in both files, and nothing was dropped.
- 36 of 38 files are blob-equal at
- Card: closed
completedby the PR'sFixesline. That was the only closing keyword in the body. This act removespm:dispatchedand the assignee. - What now holds (ruling
5974483403, Q1-B + Q2-O1):JobSchemahas a third run form,pull: { mapping }, exclusive ofbodyandhandler.IAutomationService.pullConnectorSourceis served by the automation engine, and the one binder schedules a pull job and calls it through the service registry.defineStack/os validaterefuses a pull naming an undeclared mapping.- A job declares
organizationunder the scheduled-flow posture rule, judged at bind, and its body, handler and pull run as that organization. - On
isolatedwith package scheduled work on, a packaged job with noorganizationis not scheduled, as the changeset states.
- Ruling A, complete: ① spec (feat(spec)!: connector-attached sync leaves the connector — syncConfig / fieldMappings retired, mapping gains the connectorSource pull binding (#20281 stage 1) #20903), ② service (feat(service-automation,core,types): the connector sync executor pulls a mapping's connectorSource through the import runner, moved beside bulkWrite #21084), ③ the job that drives it (this PR).
- Follow-up filed: install-local accepts a package whose job
pullcannot bind: it answers 200 and the job is never scheduled. Refuse it at the door, as a body job that does not bind is refused #21672. Install-local answers200for a pull job that cannot bind; the authoring doors already refuse that shape. Bare for triage.
- Landed: PR feat(spec,runtime,service-automation): a job pulls a mapping by declaration (pull: { mapping }) and runs as its declared organization (#20281 stage 3) #21668 merged through the merge queue at 2026-10-04T03:16Z as
- added 5 commits that reference this issue
on Oct 7, 2026
Ruled: 5974483403 · letters Q1-B + Q2-O1 · 2026-10-03T23:15Z
Ruled: 5904845660 · letter A — the sync definition lives on the target-side
mappingtype (source binding +runImport+jobcadence); the 14 connector keys retire under ADR-0087; v1 one-way pull; three staged PRs (spec → service → job) · 2026-09-30T05:40ZFiling gate: ① a declared≠enforced family, filed as one sweep card per family under ruling A′ item ④ on #18900 (
5727134555). This is triage's standing request5857165909on the seat post. Familyconnector-sync, seat verdict ENFORCE.reach:the declared authoring door.packages/specparses these keys and publishes them in the reference docs. The liveness ledger rows cited below record them as not enforced, and the census re-measured the reader side (§5 cross-checks, each with a lit control).Census by the
domain:specexecution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017), 2026-09-27. Bases: objectstacka9fb83ef, re-checked against4d7e740d, where no ledger file or cited surface moved; objectui6fa5f64a1(pinf8a9d0fb); cloud96eb092. Ledger instrument:check-liveness.mts --json, whosebyStatusequals the committedstate-counts.mdrow for row. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. The ranking is by value, user-visible risk × keys. This family's rank is2of 16. The sibling family cards filed so far are #20273 and #20274.Capability: Declarative data sync (replication) from an external system into a local table: strategy (full/incremental/upsert/append), direction, watermark field, conflict policy, batch size, delete mode and a source→target field map
connector.syncConfig.strategypackages/spec/liveness/connector.json:150syncConfighas NO reader outside packages/spec, and this ledger re-measured it rather than inheriting the claim: the only non-spec occurrence of the word in packages/ or examples/ is a prose comment in packages/qa/dogfood/test/expression-conformance.ledge…connector.syncConfig.directionpackages/spec/liveness/connector.json:155syncConfig.strategy— no connector-attached sync engine exists.connector.syncConfig.realtimeSyncpackages/spec/liveness/connector.json:160syncConfig.strategy. Thewebhookssubtree below is the mechanism this flag claims to enable, and it is dead on this type too.connector.syncConfig.timestampFieldpackages/spec/liveness/connector.json:165syncConfig.strategy— nothing tracks a last-sync watermark for a connector.connector.syncConfig.conflictResolutionpackages/spec/liveness/connector.json:170syncConfig.strategy. Its default (latest_wins) is the kind of value that reads as a configured policy and resolves nothing.connector.syncConfig.batchSizepackages/spec/liveness/connector.json:175syncConfig.strategy— no bulk path reads it.connector.syncConfig.deleteModepackages/spec/liveness/connector.json:180syncConfig.strategy.soft_deleteby default, applied by nothing.connector.syncConfig.filterspackages/spec/liveness/connector.json:185syncConfig.strategy. Declaredz.record(z.string(), z.unknown()), so it is not a container for this walk and no subtree rides on this verdict.connector.fieldMappings.sourcepackages/spec/liveness/connector.json:194field-mapping-transform-removedentry (that is the id in full — noconnector-prefix; theconnector--prefixed neighbour isconnector-rate-limit-config-removed, a different retirement) me…connector.fieldMappings.targetpackages/spec/liveness/connector.json:199fieldMappings.source.connector.fieldMappings.defaultValuepackages/spec/liveness/connector.json:209fieldMappings.source.connector.fieldMappings.dataTypepackages/spec/liveness/connector.json:214fieldMappings.source. SYNC_ARCHITECTURE.md's corrected note says an L3 mapping 'declaresdataTypeandsyncModeand performs no value transformation' — true about the transform half, and this row records that the dec…connector.fieldMappings.requiredpackages/spec/liveness/connector.json:219fieldMappings.source. A.default(false)that no validation consults.connector.fieldMappings.syncModepackages/spec/liveness/connector.json:224fieldMappings.source.read_only/write_only/bidirectionalgate nothing — there is no sync to gate.Mainstream evidence:
coalesceas the upsert key, and scheduled data imports.connectorframework is a community add-on, UNVERIFIED as mainstream. Retool / Appsmith: none (they query live).Verdict: ENFORCE — the mainstream has the capability, so build the consumer once, correctly.
Reader that must exist / disposition: NEW in objectstack: a sync executor, in packages/services/service-automation or a new service, that reads
syncConfig+fieldMappingsfrom a materialized declarative connector, schedules it and writes through ObjectQL. Today nothing outside packages/spec reads either key (0 code hits; see cross-check C1).User-visible risk (2): The docs advertise the capability (SYNC_ARCHITECTURE.md L3, content/docs/references/integration/connector.mdx), and the defaults (
conflictResolution: latest_wins,deleteMode: soft_delete) read like configured policy while nothing applies them.Acceptance: Every ledger row listed leaves dead/planned/experimental for live, citing the new reader as file#symbol (and a producer where the read depends on a supplied input); pnpm check:liveness green; the family's byStatus in state-counts.md regenerated.
Lane: domain:spec parent (contract) + domain:services sub-issue (executor), both in objectstack
File surface: packages/spec/src/integration/connector.zod.ts:946,952 (DataSyncConfigSchema, ConnectorFieldMappingSchema) · packages/services/service-automation/src/engine.ts#registerConnector · packages/spec/docs/SYNC_ARCHITECTURE.md · packages/spec/liveness/connector.json
Dedupe:
syncConfig \| fieldMappings \| ConnectorFieldMapping \| DataSyncConfig \| conflictResolution \| timestampField→ 0 open hits.四轴:
Blocked-by: #20919