Skip to content

spec(integration): build the connector sync executor that syncConfig and fieldMappings declare (14 keys), once and on the mainstream shape #20281

Description

@objectstack-fleet

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.

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, coalesce as 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 connector framework 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.

四轴:

Blocked-by: #20919

Activity

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: approvals and automation | 缺项 (no item authors a connector syncConfig / fieldMappings) | P2

    Triage: first grade — enhancement · priority:p3 · domain:spec · pm:queue. Verdict: ENFORCE, by the maintainer's criterion

    Triage: the parent lands in packages/spec/src/integration/connector.zod.ts and the ledger. The consumer is new: a sync executor (in service-automation or a new service) that reads syncConfig + fieldMappings from a materialized declarative connector, schedules it and writes through ObjectQL ⇒ domain:spec parent, with a domain:services sub-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.

    1. 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.
    2. Until it ships, SYNC_ARCHITECTURE.md stops presenting the capability as delivered (a one-line honesty fix that rides the first PR).
    3. ⛔ Stop valve: if the executor needs a credential or scheduling contract the spec does not declare, stop and report here; do not invent one.
    4. Not folded with spec(integration): retire the connector health-probe, circuit-breaker, authored status and nested webhooks keys (16), which nothing enforces #20273 (RETIRE, the same schema file). The first to claim goes first, and the other merges main.
  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-decision-facets

    决策请求:连接器数据同步做成什么样?定义放在目标表一侧(复用现有的导入映射),还是留在连接器上补齐,还是不做? · 2026-09-30T03:51Z

    domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1)。分诊 5859569880 已按你的标准定为 ENFORCE(做)。但它的第 3 条执行说明是停止阀:「执行器需要 spec 没有声明的凭证或调度约定时,停下来报告,不许自己发明」。本席的只读普查(本地 c-20281/census.md,基于 origin/main 0d9349fe)测得停止阀在动手之前就已触发,所以把方向放进决策箱。本卡同时转入 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 删掉的定时键。

    前提(每条带复查方法)

    1. 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 的注释。没有示例应用写这两个键(普查)。
    2. 协议里缺的东西(普查 §1、§3、§4): 目标对象、数据源的读取方式(列表、分页、水位)、匹配键、执行节奏、执行者所属组织、水位存在哪。运行时注册连接器时会丢掉 syncConfig 和 fieldMappings(普查:service-automation/src/plugin.ts:1674)。
    3. 目标一侧已经有一半是活的。 mapping 元数据类型(packages/spec/src/data/mapping.zod.ts:310-317)已有目标对象、字段映射、写入模式(新建/更新/新建或更新)和匹配键,台账 7 个键全部 live。REST 导入器 runImport 已经按匹配键做「有则更新、无则新建」,并拒绝匹配到多条的行(普查 §4.3)。
    4. 有的默认值根本没有运行时。 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

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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 request 5903692970 (recommendation A) after this seat's own reading of the census facts: packages/spec/src/data/mapping.zod.ts already declares targetObject, fieldMapping, mode (insert / update / upsert) and upsertKey, all live; connector.zod.ts:295-306 records the 2026-09-10 deletion of syncConfig.schedule ("sync on a cadence is a job"); runImport lives in packages/rest/src/import-runner.ts; triage 5859569880 ruled ENFORCE with the stop valve this request tripped.

    Ruled: A. The sync definition lives on the TARGET side: the existing mapping metadata type gains a source binding that names the connector it pulls from; writes reuse runImport's upsert-by-match-key; cadence is a job (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:p3 stays — this ruling fixes the direction, not the schedule.
    • AI-safety: soft_delete / latest_wins stop reading as configured policy; a connector-side syncConfig written 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 / openapi connectors only.
    • Write: runImport. The executor lives in service-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 — mapping gains the source binding and the 14 connector keys retire (Clause-②: yes, BREAKING banner, ADR-0087 disposition), and SYNC_ARCHITECTURE.md stops presenting the capability as delivered in the same PR; ② service — the executor; ③ the job driving it.
    • Not folded with spec(integration): retire the connector health-probe, circuit-breaker, authored status and nested webhooks keys (16), which nothing enforces #20273 (RETIRE, the same schema file): the first to claim goes first; the other merges main.
    • Confidence gaps recorded: real customer sync targets unmeasured; the runImport dependency direction unmeasured; v1 connector kinds set at design.

    State: needs-user-decision → pm:queue (domain:spec, priority:p3 stay); the Ruled line goes on the body in the same stroke. The domain:spec seat that filed the request writes these parameters into the card's dispatch and files the stage cards as it claims.

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: 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 as GET /user answers 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 published strictObject, which is a narrowing. The new mapping binding widens the accept set.


    Generated by Claude Code

  5. 32 remaining items

  6. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (stage ③ of ruling A 5904845660, built to the maintainer's ruling 5974483403: Q1-B + Q2-O1) · 2026-10-03T23:58Z
    Session: session_01T9u38rswFp5Rw8DswRUReJ
    Account: os-project-manager (the seat's linked user as GET /user answers 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 (at origin/main 15fe567c9c; stop on breach and explain in the report):

  7. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  8. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 36da2bfc8882a5fbbb9d5277f7e37d8a18587f8b
    Local-runs: none

    PR #21668 (draft, 38 files, +1552 / −107, no governed path) on card #20281 — stage ③ of ruling A (5904845660), built to the governing ruling 5974483403 (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 against main at fea67065a3, 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 report 5975609901 was read as a claim and tested against the diff.

    Check-runs on the head, read at 2026-10-04T02:15Z: 27 success, 2 skipped (Console Pin Gate and Packed-tarball smoke — path- and label-filtered, as on the stage-① head), 0 failure, 5 still in_progress: Lint & Repo Gates, and Test Core shards 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; also Check 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.

    1. JobSchema.pull — a closed { mapping } with mapping: SnakeCaseIdentifierSchema, optional. Right. Q1-B's third run form, a widening of the authorable accept set. The name pattern is the same schema mapping.name uses (mapping.zod.ts:332), so no declared mapping name is unreachable from pull.mapping. Closed from day one; an unrecognized key inside it is refused.
    2. 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 present body wins" (job.zod.ts:234, :295 at base) — AT-LEAST-ONE, not exactly-one. The ruling says pull is "mutually exclusive with body and handler; writing both is refused". The diff does exactly that: requiredOneOf widens to the three keys (a pull-only job now parses), and a new refinement refuses pull beside either, located at pull. body + handler stays 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 in dropped-refinements.baseline.json (660 → 665: system/Job root plus the four manifest.jobs.element echoes), and the ledger note on pull says so. Right.
    3. The alias table. Right. mapping / sync / connectorSource → pull, and organizationId / orgId / tenantId / tenant → organization (the probe folds case and underscore, so the snake spellings are caught too); on pull itself name / 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.
    4. JobSchema.organization — ScheduleOrganizationSchema by 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 on pull). Optional at parse by design: whether it is required is the deployment's posture, which is not knowable at authoring.
    5. defineStack → validateCrossReferences gains collectJobPullMappingErrors. Right. The ruling's "os validate checks the mapping name" — os validate loads through loadConfig and refuseUnbuiltStack, so defineStack's own cross-field refusals are the validator it relies on (validate.ts:246). mappings is z.array(MappingSchema) on the definition (stack.zod.ts:708) and the existing loop at :2917 reads it as an array, so the new array read cannot false-refuse a record form. Both halves refused (undeclared name; declared mapping with no connectorSource), with the existing STACK_CROSS_REFERENCE_INVALID / 422 envelope; placed ahead of the objectNames.size === 0 early 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.
    6. IAutomationService.pullConnectorSource? plus ConnectorSourcePullRequest / ConnectorSourcePullResult / ConnectorSourcePullSummary. Right. An additive, optional contract member (the getConnectorDescriptors precedent, with the same reason stated), context?: ExecutionContext typed from the kernel shape, three new exports with api-surface/contracts.json and export-origins/contracts.json regenerated. The result type exposes the runner's tallies and not its per-row results, which the integration pin now reflects.
    7. AutomationEngine.setConnectorPullSource / pullConnectorSource. Right. The registered automation service is the engine, so the contract method lands there and the plugin hands it the call at init() before registerService — the materialized-connector map stays the plugin's. A bare engine rejects with SERVICE_UNAVAILABLE 503 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.
    8. The ONE binder (scheduleAppArtifactJobs). Right. judgeJobPull judges a pull against the artifact's resolved collections (ADR-0130 D4, sibling packages included — pinned) and the parsed JobSchema.pull; a pull that does not bind is not scheduled and the reason is said at warn with the key named (pull.mapping). Each run resolves the automation service again through the registry (pinned) and maps the outcome once (pullRunOutcomeOf): a refused pull rejects → failed and retryPolicy; refused rows → degraded with the counts; otherwise completed, an empty pull included. A composition with no pull door is not scheduled, naming the plugin to compose. Result gains pulls; retirement sets include pulls. collectJobsWithoutBody never names a pull job — right, because naming it would prescribe a body the schema refuses beside pull.
    9. 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-63 at base: "⛔ do not re-add an authoring-time lint for it: at authoring time neither the switch nor the posture is knowable". The binder imports resolveScheduledWorkPolicy from @objectstack/types and reads requiresActingOrganization and runOwnership === 'per-record' — the very reads schedule-trigger.ts:503, :738 make. A reuse, not a copy. describeMissingJobOrganization is a sentence of its own because the flow's names a start node's config; 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 validate on the mapping name and the exclusion, and says only that the organization declaration is checkable — consistent.
    10. jobExecutionContext(org) on every run form. Right for body (buildJobSandboxContext takes the organization; the real-sandbox pin shows { isSystem: true, tenantId } on the write) and for pull (the request's context). For handler the envelope is HANDED as JobHandlerContext.executionContext and ql stays the raw engine, pinned as such by app-plugin.job-data-reach.test.ts (ql IS 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.
    11. Behaviour change on an isolated deployment. Right, and ruled. With package-authored scheduled work switched ON under isolated, a packaged job declaring no organization was scheduled before and is now NOT scheduled, logged at error with the remedy, counted in missingOrganization. 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 under isolated" (the 2026-09-08 ruling extended to jobs) and the switch is OFF by default in every posture. group: scheduled, named once at warn (per-record ownership has no record on a job). single: unchanged. Stated in the changeset with the operator action. Right.
    12. Unreadable posture, switch on: withheld: 'scheduled-work-policy-unreadable', the app's jobs retired, said at error. Right. resolveTenancyPosture already refuses an unrecognized OS_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 to single and drop the requirement), and retiring the app's jobs on a withholding path is what the base binder's scheduled-work-disabled path already does (:525 at base). Stated in the changeset.
    13. Runtime public surface. Right. AppArtifactJobScheduling gains pulls and missingOrganization and one withheld value; JobHandlerContext.executionContext? is additive; jobBodyRunnerFactory's job shape gains organization?; judgeJobPull, resolveJobOrganization, jobExecutionContext, pullRunOutcomeOf are exported. The result-shape pin and the context-keys pin moved with the change. The cloud-connection reader of the result was run against the new dist by the dev (11 passed), not re-run here.
    14. Texts the change made false, corrected. Right and complete against the ruling's list: mapping.zod.ts TSDoc and connectorSource describe, connector.zod.ts SYNC_CONFIG_RETIRED (a tombstone prescription — a runtime string, still tracker-free), the D3 entry and the regenerated migrations/registry.ts, SYNC_ARCHITECTURE.md (five places, with a Job specimen beside the Mapping one), connector-pull.ts, plugin.ts, service-automation/src/index.ts, the lint/authoring-rules.ts comment, the mapping.json note, and the two pins that asserted "nothing schedules … yet", now asserting the absence of that sentence. hook-bodies.mdx untouched (its planned line belongs to Q1-A) and releases/ untouched — both right.
    15. Liveness and generated artefacts. Right. liveness/job.json gains pull (drilled: mapping live, four evidence anchors, two producers) and organization (live, evidence on the binder, the envelope builders and the tenancy guard; producer resolveScheduledWorkPolicy); state-counts/job.md 19 → 21 live, 21 → 23 classified; Spec property liveness is green on the head. authorable-surface/system.json gains the two keys; the strictness-ledger count moves by one; the three reference pages and system-context.mdx (declared-count 23 → 25, the two new { isSystem: true; tenantId? } declarations) are regenerated, not hand-written.
    16. Docs. Right. content/docs/automation/jobs.mdx gains "Pulling a mapping" and "The organization a job runs as" (the posture table matches the binder limb for limb); no new page, so no meta.json move 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/spec minor, @objectstack/runtime minor, @objectstack/service-automation minor. 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. minor is the floor Clause-②: yes demands and the ceiling the diff reaches: nothing authorable narrows, so no BREAKING banner and no ADR-0087 disposition are owed, and none is written. Not skip-changeset — three released packages publish. Check Changeset is 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 or defineStack-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 the isolated bind refusal of a job with no organization: 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_conflicts 1 — "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_conflicts 2 — posture rule at bind, not at parse / os validate. Answered: the dev is right, see ① item 9. os validate checks the mapping name and the exclusion; the posture rule is the scheduled flows' bind-time rule, reused by import.
    • mechanism_assumptions 1–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 the collectJobsWithoutBody skip, measured through the real door with a deleted probe (3); job.json rows 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.ts and job-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's ctx.api without them); stack.zod.ts (the ruling's "os validate checks the mapping name", placed in the existing validator os validate already 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 where check:generated proved 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 touching cloud-connection was 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_questions 1 — should os package install refuse 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 — defineStack and os validate refuse the authoring shape (① item 5) — and the binder says why at warn, 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 (a cloud-connection describeUnrunnable clause plus the runtime half naming a non-binding pull with its own pullRefusal from judgeJobPull), and it crosses the cloud lane, 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-existing refinementSitesThatDidProject drift in the dropped-refinements header (369 vs a measured 444 — the pin holds the other two counts), the ungated rows of system-context.mdx, and the connector.json syncConfig note 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 handler form 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: no hook.zod.ts edit, 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_01T9u38rswFp5Rw8DswRUReJ

    VERDICT: PASS


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21668 at 36da2bfc88 (stage ③, built to ruling 5974483403: Q1-B + Q2-O1)

    domain:spec seat 1 · os-project-manager · session session_01T9u38rswFp5Rw8DswRUReJ · 2026-10-04T02:18Z · the review of record for the stage-③ report on this card; contract review PASS 5975694925 (at-tier, isolated, on this head).

    Checklist (read on GitHub, not from the report):

    The two dispatch conflicts, answered for the build:

    1. 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). pull excludes both, and the old pair is unchanged; narrowing it would be breaking. That satisfies the ruling's "mutually exclusive with body and handler". The dispatch's "exactly-one" was the seat's misstatement.
    2. Where the organization rule runs. The rule runs at bind, reusing resolveScheduledWorkPolicy (the same reads schedule-trigger.ts makes) and ScheduleOrganizationSchema, not a copy. The posture and the scheduled-work switch are deployment facts, and schedule-organization.zod.ts forbids an authoring-time lint for them. The ruling says to use the rule scheduled flows use. os validate checks the mapping name.

    The behaviour change, accepted as ruled: on an isolated deployment with package scheduled work on, a packaged job that declares no organization is 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 200 and 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 into packages/cloud-connection, outside this claim, so it is not built here.

    Out-of-scope findings, noted, not filed (no carrier): the pre-existing refinementSitesThatDidProject header drift (369 vs 444), the ungated system-context.mdx rows, and the stale narrative in liveness/connector.json's syncConfig note. None is made false by this change.

    Landing: CI on 36da2bfc88 reads 31 success, 2 skipped and Test 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 merges main.

  10. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: stage ③, PR #21668 → 909229e976. Ruling A's three stages are complete

    domain:spec seat 1 · os-project-manager · session session_01T9u38rswFp5Rw8DswRUReJ · 2026-10-04T03:16Z · holder of claim 5974797810.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions