Repository navigation
Description
Ruled: 5974483403 · letters Q1-B + Q2-O1 · 2026-10-03T23:15Z
Ruled: 5904845660 · letter A — the sync definition lives on the target-side mapping type (source binding + runImport + job cadence); the 14 connector keys retire under ADR-0087; v1 one-way pull; three staged PRs (spec → service → job) · 2026-09-30T05:40Z
Filing gate: ① a declared≠enforced family, filed as one sweep card per family under ruling A′ item ④ on #18900 (5727134555). This is triage's standing request 5857165909 on the seat post. Family connector-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).- The criterion is the maintainer's: 「每族该问的是:主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」.
- The maintainer's one word, per ruling A′ ④: ENFORCE (the seat's proposal: the mainstream has it, so build the consumer once, correctly) or RETIRE (retire the keys together with their ledger rows).
⚠️ This verdict reverses the pre-A′ direction. L1 Simple Sync (spec 双源清账 C13+C15(一单):DataSyncConfig + ConflictResolution(automation 死删 / integration 改名 / ui 保名)—— 4 条 #4738) and L2 ETL (L2 ETLPipeline 在本仓无任何执行侧消费者(仅 spec 自身 + 生成文档),而 SYNC_ARCHITECTURE.md 把它作为 L1 退役后的推荐去处 #6414) were retired on the old "no consumer" criterion. The mainstream test says sync is a mainstream capability, so the maintainer's word here overrides that precedent. Every mainstream platform binds the sync definition to the TARGET table or dataflow, not to the connection, so "once, correctly" includes moving the shape.
Census by the domain:spec execution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017), 2026-09-27. Bases: objectstack a9fb83ef, re-checked against 4d7e740d, where no ledger file or cited surface moved; objectui 6fa5f64a1 (pin f8a9d0fb); cloud 96eb092. Ledger instrument: check-liveness.mts --json, whose byStatus equals the committed state-counts.md row 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 is 2 of 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
| key | ledger status | ledger row | what the ledger cites |
|---|---|---|---|
connector.syncConfig.strategy |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:150 |
note: syncConfig has 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.direction |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:155 |
note: Dead for the one reason recorded on syncConfig.strategy — no connector-attached sync engine exists. |
connector.syncConfig.realtimeSync |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:160 |
note: Dead for the one reason recorded on syncConfig.strategy. The webhooks subtree below is the mechanism this flag claims to enable, and it is dead on this type too. |
connector.syncConfig.timestampField |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:165 |
note: Dead for the one reason recorded on syncConfig.strategy — nothing tracks a last-sync watermark for a connector. |
connector.syncConfig.conflictResolution |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:170 |
note: Dead for the one reason recorded on syncConfig.strategy. Its default (latest_wins) is the kind of value that reads as a configured policy and resolves nothing. |
connector.syncConfig.batchSize |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:175 |
note: Dead for the one reason recorded on syncConfig.strategy — no bulk path reads it. |
connector.syncConfig.deleteMode |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:180 |
note: Dead for the one reason recorded on syncConfig.strategy. soft_delete by default, applied by nothing. |
connector.syncConfig.filters |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:185 |
note: Dead for the one reason recorded on syncConfig.strategy. Declared z.record(z.string(), z.unknown()), so it is not a container for this walk and no subtree rides on this verdict. |
connector.fieldMappings.source |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:194 |
note: Re-verified, not inherited: the ADR-0087 conversion registry's field-mapping-transform-removed entry (that is the id in full — no connector- prefix; the connector--prefixed neighbour is connector-rate-limit-config-removed, a different retirement) me… |
connector.fieldMappings.target |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:199 |
note: Dead for the one reason recorded on fieldMappings.source. |
connector.fieldMappings.defaultValue |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:209 |
note: Dead for the one reason recorded on fieldMappings.source. |
connector.fieldMappings.dataType |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:214 |
note: Dead for the one reason recorded on fieldMappings.source. SYNC_ARCHITECTURE.md's corrected note says an L3 mapping 'declares dataType and syncMode and performs no value transformation' — true about the transform half, and this row records that the dec… |
connector.fieldMappings.required |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:219 |
note: Dead for the one reason recorded on fieldMappings.source. A .default(false) that no validation consults. |
connector.fieldMappings.syncMode |
dead (verified 2026-09-17) | packages/spec/liveness/connector.json:224 |
note: Dead for the one reason recorded on fieldMappings.source. read_only / write_only / bidirectional gate nothing — there is no sync to gate. |
Mainstream evidence:
- ServiceNow Import Sets + Transform Maps: field maps from source to target,
coalesceas the upsert key, and scheduled data imports. - Microsoft Power Platform dataflows (Power Query) loading Dataverse tables, with scheduled refresh and incremental refresh on a date column (the watermark).
- Airtable Sync (synced tables from another base or from external sources). Two-way sync for some sources is UNVERIFIED.
- Salesforce Data Cloud data streams, with refresh mode full refresh or upsert. Salesforce Connect is the virtualization alternative (no copy).
- Odoo: no declarative external sync in core. The OCA
connectorframework is a community add-on, UNVERIFIED as mainstream. Retool / Appsmith: none (they query live). - Shape note: every one of these binds the sync definition to the TARGET table or dataflow, never to the connection. "Build it once, correctly" (一次做对) therefore includes deciding whether the shape stays on the connector.
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 + fieldMappings from 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.
四轴:
- 实际业务需求: 把外部系统的数据定期同步进本地表,是 ServiceNow Import Set、Power Platform dataflow、Airtable Sync 的核心用法。有连接器、没有同步,集成场景只能靠手写 flow 拼出来。
- 项目长远合理性: 分量最重的一轴。元数据平台的集成层迟早要有同步能力,一次按主流形状做对(同步定义挂在目标表 / 同步任务上,连接器只负责连接)比保留一个悬空声明更站得住。
⚠️ L1 Simple Sync(spec 双源清账 C13+C15(一单):DataSyncConfig + ConflictResolution(automation 死删 / integration 改名 / ui 保名)—— 4 条 #4738)与 L2 ETL(L2 ETLPipeline 在本仓无任何执行侧消费者(仅 spec 自身 + 生成文档),而 SYNC_ARCHITECTURE.md 把它作为 L1 退役后的推荐去处 #6414)是在 A′ 裁决之前按「无消费者」退役的,本卡按新标准给出相反结论,需要维护者一字确认。 - 防 AI 写错: 在执行器落地之前,这 14 个键应当 authorWarn 或拒收。否则 AI 按文档写同步配置,会得到「看似配置完成」的静默空转。
- 创业阶段不扩散: 这是本普查里造价最高的一族(需要调度、水位、冲突处理)。若维护者判定现阶段不做,诚实的替代是按族退役,而不是继续挂着。
Blocked-by: #20919
Activity
objectstack-fleet commentedon Sep 27, 2026
Path: 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.
SYNC_ARCHITECTURE.mdstops presenting the capability as delivered (a one-line honesty fix that rides the first PR).statusand nestedwebhookskeys (16), which nothing enforces #20273 (RETIRE, the same schema file). The first to claim goes first, and the other mergesmain.objectstack-fleet commentedon Sep 30, 2026
os-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 条停止阀(原文见上)。packages/spec/src/integration/connector.zod.ts:295-306(维护者 2026-09-10「直接删」裁决时写下):syncConfig.schedule在 spec 17 被直接删除,没有墓碑,也没有 D2/D3;定时同步应当是一个job(Job.schedule.expression,平台唯一会执行的定时位),由它的处理函数驱动连接器。strictObject(破坏性,要走 ADR-0087);B 会重新加回 09-10 删掉的定时键。前提(每条带复查方法)
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的注释。没有示例应用写这两个键(普查)。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)。选项与代价
mapping类型,加「数据来自哪个连接器」。写入复用runImport。节奏按 09-10 裁决的方向,由job驱动,不加回定时键。连接器上的 14 个键按 ADR-0087 迁移或退役。第一版只做单向拉取(外部 → 本地),全量或按时间戳增量syncConfig加目标对象、数据源、匹配键,并加回定时键job处理函数,调现有导入器业务直译:A = 「导入」和「定期同步」是同一件事,只是后者按时自动跑;B = 同步挂在连接上,与导入分开维护;C = 平台不管同步,谁要谁自己写。
四轴(业务立场)
mapping与runImport,不新造第二套映射,特例最少。B 让导入映射和连接器同步各有一套,还要推翻 09-10 的裁决。soft_delete、latest_wins这些默认值读起来像已配置的策略,实际什么都不做。A 和 C 都会让它们退出连接器,再写就被响亮地拒绝;B 要一个个补上运行时,否则仍然静默。推荐
A。 只看①选 A;②③④ 是否翻转:否(②只把它留在 p3,晚些排)。回退:C。
置信缺口:没有测过真实客户要同步哪些系统;
runImport在@objectstack/rest里,执行器从服务层调用它的依赖方向还没核实;第一版只支持哪类连接器,要等设计阶段量。裁后执行(维护者只裁方向)
rest/openapi连接器。runImport。service-automation。mapping加数据源绑定 + 连接器键退役)→ 服务层执行器 →job驱动。每阶段一张 PR、达档复核、落地。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 commentedon Sep 30, 2026
Ruling: 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:
priority:p3stays — this ruling fixes the direction, not the schedule.soft_delete/latest_winsstop reading as configured policy; a connector-sidesyncConfigwritten after this is refused loudly with the prescription.Execution parameters (ruled in the same stroke — the seat's nine forks, each a parameter the maintainer may overturn on this card):
rest/openapiconnectors only.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 ②.deleteMode: soft_delete,conflictResolution): narrow the enum or change the default, so nothing reads as configured that does nothing.retiredKey()tombstones, one D2 conversion, one D3 entry — in the spec stage.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.statusand nestedwebhookskeys (16), which nothing enforces #20273 (RETIRE, the same schema file): the first to claim goes first; the other mergesmain.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.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 24 (stage ① of ruling A
5904845660: the spec stage) · 2026-09-30T14:11ZSession:
session_01Sfe5YjBLwB9J3y8fvm2xq1Account:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-20281-mapping-source-bindingWorktree:
objectstack-issue-20281-s1Domain:
domain:specSeat:
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 published
strictObject, which is a narrowing. The newmappingbinding widens the accept set.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.jobdrives the cadence, per the ruling of 2026-09-10.5903692970§6 (b)): the connector reference, the read operation, and an optional watermark field. It lands asplannedrows withauthorWarn, because stage ② builds the reader.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..changeset/20281-*.md:@objectstack/specminorwith a BREAKING header and the ADR-0087 disposition marker.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-level
options.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
32 remaining items
objectstack-fleet commentedon Oct 3, 2026
Claim: PM loop round 1 (stage ③ of ruling A
5904845660, built to the maintainer's ruling5974483403: Q1-B + Q2-O1) · 2026-10-03T23:58ZSession:
session_01T9u38rswFp5Rw8DswRUReJAccount:
os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-20281-stage3-job-pullWorktree:
objectstack-issue-20281-s3Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (at
origin/main15fe567c9c; stop on breach and explain in the report):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.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.packages/services/service-automation/src/plugin.ts,connector-pull.ts), plus tests.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.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).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 as
045b946256. 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.wip(spec,runtime,service-automation): a job pulls a mapping by declar…
objectstack-fleet commentedon Oct 4, 2026
os-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 commentedon Oct 4, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
36da2bfc8882a5fbbb9d5277f7e37d8a18587f8bLocal-runs: none
PR #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.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.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.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.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.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.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.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.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/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.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
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/lint(two comment lines, publishes no behaviour);content/docs/**,docs/audits/**and the spec's generated artefacts are not published surfaces. Right.③ 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.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-pullReviewed-by:
session_01T9u38rswFp5Rw8DswRUReJVERDICT: PASS
Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
ACCEPT — 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;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:
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.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 commentedon Oct 4, 2026
Landed: 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.909229e976. It has one parent (417443eb27) and is an ancestor oforigin/main.36da2bfc88(contract review PASS5975694925, ACCEPT5975717250):909229e976.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.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.completedby the PR'sFixesline. That was the only closing keyword in the body. This act removespm:dispatchedand the assignee.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.organizationunder the scheduled-flow posture rule, judged at bind, and its body, handler and pull run as that organization.isolatedwith package scheduled work on, a packaged job with noorganizationis not scheduled, as the changeset states.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.fix(spec): the etl-pipeline and unresolvable-where-column migration e…
feat(spec)!: connector-attached sync leaves the connector — syncConfi…
docs: shared picklist — NORTH-STAR feature line, two planned records-…
feat(service-automation,core,types): the connector sync executor pull…
feat(spec,runtime,service-automation): a job pulls a mapping by decla…