Repository navigation
feat(spec)!: connector-attached sync leaves the connector — syncConfig / fieldMappings retired, mapping gains the connectorSource pull binding (#20281 stage 1) - #20903
Conversation
…orSource binding (#20281 stage 1) Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…d defs Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…retirement Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…pping-source-binding # Conflicts: # packages/spec/liveness/README.md
…the connector sync retirement The merge of origin/main (which carries #20868's registry entry) owed a regeneration of three os-regen artifacts; they are regenerated here from the merged tree. The same commit writes this branch's two registry hunks, which the serial constraint held until that landing: the generated regions of migrations/registry.ts (gen:migration-registry over the entry files), the D2 conversion connector-sync-keys-removed in conversions/registry.ts (MAJOR_18_CONVERSIONS, order 50) and its STEP18_RATIONALE fragment (order 52). gen:schema cannot run without the second half (build-schemas gate (b)), so the two cannot be split. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
… replayed on both legs Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…authoring Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…pping-source-binding # Conflicts: # packages/spec/liveness/README.md # packages/spec/src/type-alias-convention.pin.test.ts
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f3801fbee5d8555bebcd4a4d16fd10c6eae0185d && git checkout f3801fbee5d8555bebcd4a4d16fd10c6eae0185d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin da93a8b3e0bfa0ac5d4d68059b419435eb7c8371 674abedfd9b5a7b139cb13434673ea1f92c7947e && git checkout -B drift-repro da93a8b3e0bfa0ac5d4d68059b419435eb7c8371 && git merge --no-ff 674abedfd9b5a7b139cb13434673ea1f92c7947e
node scripts/docs-audit/affected-docs.mjs --json da93a8b3e0bfa0ac5d4d68059b419435eb7c8371
|
Contract reviewServed-tier: Read: card #20281 (body + all six comments: triage ① Derived judgmentsA. Accept-set and public-surface changes the diff implies — each judged
B. The binding against each execution parameter of ruling
C. Author-shown and AI-facing text, sentence by sentence — what is false, unsourced or over-broad, and when each is true Tested: both tombstone prescriptions, the four
One more reading note: the code comments and the changeset cite "measured on ② Semver level
Clause-②: yes (narrowing) ③ Boundary flagsDev deviations (
Open questions (each answered):
Out-of-scope findings (each answered):
Two review-side flags for the seat, neither blocking:
Check-runs on Implemented-by: VERDICT: PASS Adopted and posted by
Generated by Claude Code |
… contract, and an identity-only column is judged as the type it renders (objectstack-ai#20901) (objectstack-ai#20927) Closes objectstack-ai#20901 Clause-②: yes (narrowing) - `FormViewSchema.subforms[].columns` references `InlineGridColumnSchema` (was `z.array(z.any())`): the card's typed `currency` + `scale` column and its `zzz_not_a_key` column are refused at the view parse. - `defineStack`'s cross-reference check (`packages/spec/src/stack.zod.ts#collectHydratedInlineColumnErrors`, called from `validateCrossReferences`) re-parses a column that declares no `type` as the type it renders (`currency` over a `currency` child field) through `InlineGridColumnSchema`, on both carriers. The card's identity-only column with `scale` is refused there with the column schema's own message; there is no second `scale` rule. - ADR-0087: D3 entries `form-view-subform-columns-closed` and `inline-grid-column-identity-only-currency-scale-refused`; the registry was regenerated after merging a `main` that carries objectstack-ai#20903 and objectstack-ai#20911. Evidence at `feba1a99bd`: pins `packages/spec/src/inline-grid-column-carriers.test.ts` 13/13; `@objectstack/spec` suite 584 files / 17179 tests green; `@objectstack/spec` typecheck green; 114/114 derived gates exit 0. Ablations, each restored with `git diff HEAD` empty: reference removed, 5 red; cross-reference call removed, 3 red. `os validate` on a probe stack: identity-only column, exit 1 `STACK_CROSS_REFERENCE_INVALID`; typed and bogus columns, exit 1 `STACK_SCHEMA_INVALID`; valid columns, exit 0. ## Acceptance notes - No check read `subforms[].childObject` before this change (`validateCrossReferences` read only `form.data.object`), so the child-object lookup is new here. - The identity-only check runs in `defineStack` only. A view saved through the metadata door, or a subform whose child object lives in another package, gets the schema half alone (NOT MEASURED at the save door). objectui's `@object-ui/types` mirror is still `z.any()`, and the render-time warning stays the backstop there. - `field.zod.ts`'s `scale` describe still names only the declared-type refusal. PR objectstack-ai#20908 holds that file. --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…forms items, coverage waiver; upgrade skill stops claiming a connector sync ran (objectstack-ai#20938) Part of objectstack-ai#19518 The Tier H docs follow-up for the shared picklist: the section "Also with this card" on the card, plus one skills correction carried from objectstack-ai#20281 under ruling 5916259513. The card stays open until this lands; the seat closes it by hand. Five files, no package source, nothing published from any package. ## What changes 1. **`docs/NORTH-STAR.md`**: one new line in step ② 「本地跑起来、看到」, records axis, with the card's text verbatim: `records · 共享选项集:多个对象复用同一份选项,客户按组织追加值而不改应用源码 · records-forms · picklist-shared-across-objects, picklist-org-append`. It sits after the last existing records row (search), so it ranks below every records capability that already exists. Moving it up is a priority call for the maintainer. 2. **`docs/qa/platform-checklist/areas/records-forms.json`**: two new items, both `status: "planned"`, `since: null`, `P2`, with no steps (README, "Implementation status"): - `records-forms.picklist-shared-across-objects` covers phase 1. One picklist on two objects: both are served the same resolved options, a package `picklistExtensions` value reaches both, the write door refuses a value outside the set, and a locale switch relabels the options. It carries three drafted acceptance clauses taken from the design of record and from the acceptance sentence of objectstack-ai#19519, so the runtime PR has a concrete target when it promotes the item. - `records-forms.picklist-org-append` covers phase 2. Organization A appends a value without editing the app source, A can write it, and organization B is refused. It carries no acceptance clauses, because the overlay's schema and write door are not designed. A `knownGaps` entry records that the overlay is not declared: the picklist registry row has `allowOrgOverride: false`, and ruling 5904864936 defers the overlay to its own sub-issue. 3. **`docs/qa/platform-checklist/coverage.json`**: `picklist` gets a waiver. It was UNCLASSIFIED. The waiver states the measured reason: `packages/spec/liveness/picklist.json` grades every key `planned`, and `check-stack-collection-maps` holds `picklists` and `picklistExtensions` as PENDING rows for objectstack-ai#19519. It names both planned ids and states what retires the waiver. 4. **`skills/objectstack-upgrade/references/examples-upgrade.md`**: the worked R1 walks a protocol-16 `connectors[].fieldMappings[].transform` source. Since objectstack-ai#20903 (0efbdc3), the chain removes the whole of `fieldMappings` after `transform` (`connector-sync-keys-removed`, toMajor 18, part of the default `os migrate meta --from 16` terminus), and the author meets that key's tombstone. Three sentences said otherwise, and each now says what is true: - "the parse error is the prescription" pointed at a `transform` tombstone. It now names the `fieldMappings` tombstone. - "the connector has been landing raw values for as long as it has been running" is now "no connector sync ever ran, so no value ever passed through this mapping". - The report template's "connector sync run against staging, 200 rows" is now "nothing ever read the key, so no data changes". - The report template's chain line now reads 16 → 18, the chain the default run replays. 5. **`scripts/check-platform-checklist.mjs`**: four sentences (three comments and the self-test success line) said the live ledger carries zero planned items. They now say that no `coverage.json` entry maps a planned item. ## How behaviour that does not exist yet is represented The checklist's own rules settle the status. `planned` is for a capability the definition requires and the platform does not implement or verify yet. A planned item never runs and is never coverage. Both items are planned: no runtime reads a picklist yet. The coverage rule also settles the waiver. A kind whose only items are planned is UNMAPPED, and the gate's instruction is "Add an item that RUNS, or waive the kind with a reason". An entry cannot hold both, so the planned ids are named in the waiver's prose. One consequence: `checklist-select capability:picklist` matches nothing until the waiver flips to `items`. Two content choices are the rules' to leave open. The dev report asks about both: - **Locale relabel:** it is on the phase-1 item. The card's grouping put it with the organization item, but promotion is per item and needs a passing run, and the acceptance sentence of objectstack-ai#19519 groups the relabel with phase 1. - **Organization append:** it is a planned requirement, as the card's NORTH-STAR text says. Ruling 5904864936 defers the overlay until a customer requirement measures it, so the maintainer may prefer to trim the line's second half and drop the item. ## Skill readings (`skills/**`) | reading | before (aaad682) | after (cab0e1b) | | --- | --- | --- | | `examples-upgrade.md` lines | 119 | 119 | | `examples-upgrade.md` bytes / tokens (ceil(bytes/4), ceiling 1197) | 4788 / 1197 | 4751 / 1188 | | package: all 10 `SKILL.md`, lines | 4394 | 4394 | | token ratchet, authored total | 143461 | 143452 | The additions are paid for by deleted content. Nothing was re-wrapped to buy lines. The ceiling stays 1197, with headroom 9. Lowering it is a separate edit to the ratchet script. ## Local verification, at cab0e1b - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 43 families from the five paths. All 43 ran; `--ran` answers "43 derived famil(ies) accounted for — 43 run, 0 NOT-MEASURED", exit 0. - 42 families exit 0. `check:platform-checklist` exits 1 on the pre-existing anchor below. - `check:pm-dispatch-gates` exits 0 (1976 self-test cases). - `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET: no lint or formula dist). After a locked build of those two packages it exits 0. - `pnpm check:platform-checklist` exits 1, the same as on `origin/main`: - before, 2 problems: `coverage.json · picklist: UNCLASSIFIED` and `areas/identity-auth.json: ABSENT SYMBOL … auth-plugin.ts#twoFactor`; - after, 1 problem: the same `twoFactor` anchor, which this diff does not touch; - the 5 anchors this diff adds resolve. - `pnpm gen:checklist-status` lists records-forms as 40 active and 2 planned (264 active and 2 planned overall). - `node scripts/check-skills-token-ratchet.mjs` exits 0: "examples-upgrade.md is 1188 tokens (ceiling 1197; headroom 9)". - `node scripts/pm/check-governed-merges.mjs --branch HEAD` exits 3, GOVERNED, landing tier H. Two of five paths hit the register (`skills/**`, `docs/NORTH-STAR.md`). The diff is +119 / -17. - No changeset: the diff releases nothing, because `skills/` ships through `npx skills add` and appears in no package's `files[]`. `skip-changeset` is applied. ## Acceptance notes - `scripts/check-platform-checklist.mjs` said in four places that the live ledger carries zero planned items. This PR corrects them (`cab0e1b557`); the tests are unaffected (221 assertions pass). Carrier: this PR. - `skills/objectstack-upgrade/evals/protocol-major-upgrade.json`, eval 3, was read and left unchanged. Its prompt, that the chain dropped `fieldMappings[].transform`, is still true. Its expected answer, "delete because the member never executed", is also still true, and it claims no sync run. - The card cites "ADR-0136 D2.4". No ADR-0136 exists in this repository's registry (zero hits), so neither item cites it; the design-of-record comment is cited instead. - The `Path:` lines on objectstack-ai#19518 and objectstack-ai#19519 still read 清单项待写. They can name the two ids once this lands. ## 维护者速读(草稿) 终稿以席位在本 PR 上的「维护者速读(终稿)」评论为准;下面与终稿一致。 **改了什么**: - 北极星「路上的功能点」第②步 records 组末尾,加一行「共享选项集」,文字照卡片原文。 - 平台测试清单加两个 `planned` 项(已计划,还不能跑): - 第一期:一份选项集两个对象共用、包扩展加值、越界值被拒、切语言换标签; - 第二期:组织自己追加值、本组织能写、别的组织被拒。 - `coverage.json` 给 picklist 记一条带理由的豁免。 - upgrade 技能示例改正三句话,报告模板里的迁移链改成 16 → 18。第一句原本把处方指向一个已不存在的 `transform` 墓碑;第二、三句说连接器同步跑过、落过数据,实际从来没有引擎跑过。 - 清单门禁脚本里有四处说「清单里没有 planned 项」(注释和自测提示语),改成与现状相符。 **为什么改**: - 卡片要求这一行和两个清单项随一个小文档 PR 落地。 - 规格层已合入,运行时(objectstack-ai#19519)还没做,两个项都没东西可跑。清单规则规定这种情况标 `planned`、覆盖表用豁免,不能拿 `planned` 冒充覆盖。 - 技能文字错一句就是产品缺陷(北极星优先级第 4 条)。 **风险与代价(含回滚)**: - 只改了文档、一份对外技能参考文件,以及一个门禁脚本的注释和自测提示语;不动任何包,不发版。技能文件仍是 119 行,token 从 1197 降到 1188。 - 回滚就是 revert 三个提交。回滚后,清单门禁会重新报 picklist UNCLASSIFIED。 - 两个版本都过了档位审查(`8e7420019e` 和 `cab0e1b557`),CI 全绿。 **席位意见**: - 建议合并。 - 下面三处您合并时可以顺手改: ① 「客户按组织追加值」(第二期)现在就写进路线图和清单,作为 planned 需求。席位意见是保留:文字是卡片原文,planned 不算覆盖,也不宣称已经做到。但裁决 5904864936 说的是等有实测的客户需求再立子卡。您若不想现在就把它放上路,删掉那一行的后半句和第二个清单项即可。 ② 「切语言换标签」放在第一期的项里(卡片原来归在第二期)。它在第一期就会交付,放在第二期要等很久才能验证。席位同意这样放。 ③ 新行放在 records 组最后,排序最低。这张表的顺序就是优先级,您想提前就挪一下位置。 **你要做的**: - 合并本 PR。这是 Tier H,需要您亲手合并;或者给出授权的 APPROVED,由席位落地。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ration (pull: { mapping }) and runs as its declared organization (objectstack-ai#20281 stage 3) (objectstack-ai#21668)
Fixes objectstack-ai#20281
Clause-②: yes (widening)
This is stage ③ of ruling A (`5904845660`) on this card, "the `job`
driving it". It is built to the maintainer's ruling `5974483403` (Q1-B +
Q2-O1). Stage ① (objectstack-ai#20903, spec) and stage ② (objectstack-ai#21084, the executor) are
already on `main`, so this PR finishes the plan the ruling set.
## What changed
### Q1-B: a job pulls a mapping by declaration
- **spec: `JobSchema.pull: { mapping }`** (`system/job.zod.ts`). This is
a third run form. It is closed, and `mapping` must be a snake_case name.
Writing it beside `body` or `handler` is refused at parse, at path
`pull`. The at-least-one rule now covers `body` / `handler` / `pull`.
`body` + `handler` stays legal, and the body still wins. The exclusion
is not in the closed projection list, so it is recorded as a dropped
refinement site in `dropped-refinements.baseline.json`: `system/Job`
root plus the four `manifest.jobs.element` echoes, total 660 → 665.
- **contract: `IAutomationService.pullConnectorSource?(request)`**, with
`ConnectorSourcePullRequest`, `ConnectorSourcePullResult` and
`ConnectorSourcePullSummary` (`contracts/automation-service.ts`). The
method is optional, like `getConnectorDescriptors`.
- **service-automation.** The registered `automation` service is the
ENGINE, so the engine gains `pullConnectorSource`. It serves the
executor that `AutomationServicePlugin.init()` attaches with
`setConnectorPullSource`, before the engine is registered. The plugin
keeps its materialized-connector map; the engine is handed the call. A
bare engine refuses with `SERVICE_UNAVAILABLE` (503).
- **runtime: the one binder** (`app-artifact-handlers.ts`,
`scheduleAppArtifactJobs`). A `pull` job is judged by `judgeJobPull`: no
code beside it, `pull` parses, and the artifact declares the mapping
with a `connectorSource`. Each run then calls `pullConnectorSource`
through the service registry, resolved again on every run. The outcome
is mapped once (`pullRunOutcomeOf`):
- a refused pull rejects, so the run is `failed` and `retryPolicy`
applies;
- refused rows give `{ outcome: 'degraded', reason }` with the counts;
- otherwise the run is `completed`.
A pull that does not bind is not scheduled and is logged at `warn`. So
is a pull on a kernel with no pull door. `collectJobsWithoutBody` never
names a pull job. The result gains `pulls` and `missingOrganization`.
- **`defineStack` → `os validate`.** `validateCrossReferences` gains
`collectJobPullMappingErrors`, which refuses a `pull.mapping` the
stack's `mappings` do not declare, or one whose mapping has no
`connectorSource`. It runs ahead of the no-object early return and uses
the existing `STACK_CROSS_REFERENCE_INVALID` envelope. `os validate`
reaches it because every config is `defineStack`-built
(`refuseUnbuiltStack`). This is a rule inside an existing validator; no
gate is added.
### Q2-O1: a job runs as its declared organization
- **spec: `JobSchema.organization`** reuses
`ScheduleOrganizationSchema`, the scheduled flow's value shape, by
reference. A near-miss spelling (`organizationId`, `orgId`, `tenantId`,
…) is refused at parse and pointed at the key through the closed shape's
aliases.
- **The binder judges it at bind** with `resolveScheduledWorkPolicy`
(`@objectstack/types`), the resolver the scheduled flows bind by:
- `requiresActingOrganization` (isolated, switch on): a job declaring
none is NOT scheduled and is logged at `error` (`missingOrganization`);
- `runOwnership: 'per-record'` (group): undeclared jobs are scheduled
and named once at `warn`;
- single: nothing is said.
- An unrecognized posture fails closed
(`scheduled-work-policy-unreadable`), and only when the switch is on;
with it off the posture is never read.
- **Every form runs as `jobExecutionContext(org)`**, which is `{
isSystem: true, tenantId: ORG }`, or `{ isSystem: true }` for a job that
declares none:
- the body's `ctx.api` envelope (`body-runner.ts`,
`buildJobSandboxContext`);
- the pull's `context`;
- the handler's new `JobHandlerContext.executionContext`. This member is
additive, and `ql` stays the raw engine: a handler writes as the
organization by passing it as `context`.
### Texts this change made false, now corrected
`mapping.zod.ts` (TSDoc and the `connectorSource` describe),
`connector.zod.ts` `SYNC_CONFIG_RETIRED`, the D3 entry
`18.connector-sync-keys-retired.ts` (and the regenerated
`migrations/registry.ts`), `SYNC_ARCHITECTURE.md` (five places),
`connector-pull.ts` (header and the `context` doc), `plugin.ts` (the
`pullConnectorSource` doc), `service-automation/src/index.ts`, the
comment in `lint/src/authoring-rules.ts`, the `mapping.json` ledger
note, and the two test pins that asserted "nothing schedules … yet".
`content/docs/automation/hook-bodies.mdx` is untouched: its planned
`ctx.connector(...)` line belongs to Q1-A, which was not taken.
`content/docs/releases/v17/17-6.mdx` is release-owned and accurate for
17.6.0, so it is untouched.
## Mechanism assumptions, measured
1. **The posture rule has one reusable implementation: CONFIRMED, with a
boundary.** The predicate is `resolveScheduledWorkPolicy()`
(`packages/types/src/env.ts`), and it is reused, not copied. The value
shape `ScheduleOrganizationSchema` is reused too. The refusal sentence
`describeMissingScheduleOrganization` is flow-shaped (it names the start
node's `config`), so the job has its own sentence beside the binder
(`describeMissingJobOrganization`). That is a separate sentence, not a
second rule.
2. **`pullConnectorSource` was reachable only as a plugin method:
CONFIRMED.** The contract method lands on the engine, the service the
kernel registers. The binder reaches it only through
`ctx.getService('automation')`. A booted `LiteKernel`'s `automation`
service reaches the plugin executor
(`connector-pull-service-door.test.ts`). The integration test's second
pull now goes through the service, end to end over a real `rest`
connector and SQLite.
3. **The binder file and install-local: PARTLY DISPROVED.** Unchanged,
`collectJobsWithoutBody` would have named every pull job as "has no
`body`", so `os package install` would have REFUSED every pull job, with
the wrong prescription. After this change, pull jobs are never named.
Measured through the real install-local door with a probe that is not
committed (runtime `dist/` built at `a3e9317f77`; nothing in the binder
changed after that):
- a package with a pull job 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; the binder warns `pull.mapping: this artifact declares no
mapping …`.
The door does not refuse that second case. A door refusal needs a clause
in `cloud-connection`'s `describeUnrunnable`, which is outside this
claim's file surface. See the report's open question.
4. **The liveness row: CONFIRMED.** `liveness/job.json` gains `pull`
(drilled, with `mapping` `live` plus its producer) and `organization`
(`live` plus its producer). `gen:liveness-counts` moves `job` to 21 live
/ 23 classified.
## Two places where the dispatch text and the tree disagree
- The dispatch called this "the same exactly-one rule the job already
applies to `body` / `handler`". The job applies **at-least-one**: `body`
+ `handler` is legal and the body wins. What was built follows the
ruling text: `pull` is exclusive with both, and the old pair is
unchanged. Narrowing `body` + `handler` would have been a breaking
change.
- 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, not knowable at authoring.
`schedule-organization.zod.ts` says the scheduled flows' rule lives at
bind and forbids an authoring-time lint for it, and the ruling says to
use the rule scheduled flows use. So the posture check is built at bind,
and `os validate` checks the mapping name only.
## Behaviour change to read
On an `isolated` deployment that has switched package-authored scheduled
work ON, a packaged job declaring no `organization` was scheduled before
this change, and its tenant-scoped writes were refused at the write. It
is now not scheduled, logged at `error`. This is the ruling's "required
under `isolated`". The switch is OFF by default in every posture. The
changeset states the action needed.
## Tests (HEAD `36da2bfc88`)
New suites:
- `packages/spec/src/system/job-pull-organization.test.ts` has 18 cases:
the run form, the exclusion in both pairs (the old pair stays legal),
closed shape, mapping-name shape, the `mapping` near-miss,
`organization` agreeing with `ScheduleOrganizationSchema` on every
value, near-miss refusals, and the `defineStack` envelope
(`STACK_CROSS_REFERENCE_INVALID` / 422) with its control.
- `packages/runtime/src/app-artifact-handlers.job-pull.test.ts` has 20
cases. It covers the pull schedule and run, `completed` / `degraded` /
rejected outcomes, the non-binding refusals, the sibling-package
mapping, a missing pull door, per-run service resolution, and the door
judgement. On the organization side it covers the envelope on all three
forms (the body runs in the real QuickJS sandbox), isolated / group /
single, and the unreadable posture with the switch on and off.
-
`packages/services/service-automation/src/connector-pull-service-door.test.ts`
has 3 cases: a bare engine refuses 503, the attached executor's
pass-through, and a booted kernel's `automation` service reaching the
plugin.
Pins moved with the change: `connector-sync-retirement.test.ts`,
`mapping-connector-source.test.ts`, the result-shape pin in
`app-artifact-handlers.jobs.test.ts`, and the context-keys pin in
`app-plugin.job-data-reach.test.ts` (adds `executionContext`).
`connector-pull.integration.test.ts`'s second pull now goes through the
`automation` service.
Runs, each through `scripts/pm/os-verify-lock.sh` with `VERDICT
command-exit 0`:
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: 610 files, 18075 passed, 1 todo, at `36da2bfc88`.
- `pnpm --filter @objectstack/spec typecheck`: green at `36da2bfc88`,
test layer included.
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2`: 319 files, 4550 passed, 19 skipped, at `1b0a4b3d0f`.
- `pnpm --filter @objectstack/service-automation exec vitest run
--maxWorkers=2`: 169 files, 2081 passed, at `1b0a4b3d0f`.
- runtime and service-automation `typecheck`: green, test layers
included.
- The one commit after `1b0a4b3d0f` touches only `system/job.zod.ts`'s
alias table and one spec test, both re-run.
- `cloud-connection`'s `marketplace-install-local-jobs.test.ts`, against
the new runtime `dist/`: 11 passed.
## Ablations and reverse verification (one-off; no permanent files)
Every mutation went through `node scripts/ablation-replace.mjs` (WRAP
mode) on committed code. The anchor hit was proven on disk, and the
restore was proven as blob == HEAD with an empty `git diff HEAD`. Each
subject is imported from `src/` (relative imports, no `dist/` in the
path).
| # | Mutation | Expected | Observed |
|:--|:--|:--|:--|
| A | `stack.zod.ts`: drop the `collectJobPullMappingErrors` call | the
4 cross-ref refusals red, control green | 4 failed / 14 passed |
| B | `job.zod.ts`: exclusivity predicate always true | pull+body and
pull+handler red | 2 failed / 16 passed |
| C | binder: `requiresActingOrganization` branch unreachable | both
isolated tests red | 2 failed / 18 passed |
| D | `buildJobSandboxContext`: never carries `tenantId` | the
body-organization test red | 1 failed / 19 passed |
| E | `pullRunOutcomeOf`: never degraded | the degraded test red | 1
failed / 19 passed |
| F | `collectJobsWithoutBody`: drop the pull skip | the door-judgement
test red | 1 failed / 19 passed |
Cross-package type reverse verification: a temporary
`packages/runtime/src` probe typed `{ mapping, bogusKey }` as
`ConnectorSourcePullRequest`, and `tsc --noEmit -p
packages/runtime/tsconfig.json` answered TS2353 on that line. Its other
line, which reads `IAutomationService['pullConnectorSource']`, compiled.
So the runtime typecheck read the rebuilt spec `.d.ts`. The probe was
deleted.
## Local verification
All at HEAD `36da2bfc88`, after the last commit:
- **Derived gate union.** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` printed 119 commands. All 119 exited
0, each exit code captured before any pipe. `--ran` reconciliation: "119
derived, 119 run, 0 NOT-MEASURED, 0 UNRUN" (exit 0). The union includes
`pnpm --filter @objectstack/spec run check:generated`, which checks all
15 generated artifacts.
- **The first union** ran at `a3e9317f77` and found one real drift.
`check-system-context-census` reported `[declared-count]` 23 against 25:
the two new `{ isSystem: true; tenantId?: string }` type declarations.
`pnpm gen:system-context-census` corrected
`content/docs/permissions/system-context.mdx`, and the gate is now
green. `check:skill-examples` and `check:dual-build-cjs-loads` exited 3
there (unbuilt prerequisites, so nothing was measured). Both are green
in the final union.
- **Generated spec artifacts**, regenerated by `check:generated --fix`
only where they were proven stale: `api-surface/contracts.json`,
`export-origins/contracts.json`, `authorable-surface/system.json`,
`liveness/state-counts/job.md`, the strictness-ledger `system.md` count,
`migrations/registry.ts`, and the three reference pages.
`dropped-refinements.baseline.json` was edited by hand from the build's
printed corrections.
- **Lint.** The repo-wide `pnpm lint` is CI's run. This is the proven
narrowing:
1. Population, from `eslint.config.mjs`:
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`.
2. Count: `eslint --no-inline-config --format json` over the 23 changed
lintable files reports 23 files, 0 errors, 0 warnings.
3. Invariance: the config enables no type-aware linting (its own
statement: no `parserOptions.project`, no typed rules), and this diff
edits neither the config nor a file it reads. So no verdict on an
untouched file can move.
- **Door readings, measured with throwaway probes that were then
deleted:**
- `os validate` on a config whose job pulls `orders_pul` exits 1 with
`code: STACK_CROSS_REFERENCE_INVALID`. With the name corrected, the load
passes `defineStack`. That control's exit 1 comes only from unrelated
docs-tree rules (`docs/namespace-required`, `docs/metadata-embed-ref`),
with no cross-reference error.
- The install-local door readings are in assumption 3 above.
- **Not run locally, declared for CI:** the `packages/cli` integration
tier (`package-install-local-jobs.integration.test.ts`). This diff
touches no CLI file and no spawn entry.
## Acceptance notes
- **The install-local door does not refuse a pull job that does not
bind** (assumption 3). It installs 200 and the binder warns. A refusal
is a `cloud-connection` clause (`UnrunnableCode.jobs` gains a pull
refusal, `describeUnrunnable` a sentence). Carrier: PM decision, in the
report's open questions.
- **A `handler` job writes as its organization only by passing
`executionContext`.** `ql` stays the raw engine, so existing handlers
are unchanged byte for byte. The handler form is deprecated; body and
pull carry the envelope by construction.
- **`os validate` resolves `pull.mapping` against the stack's own
top-level `mappings`**, like every mapping reference in
`validateCrossReferences`. The binder resolves against the artifact's
resolved collections (ADR-0130 D4, `packages[]` included), so the
binder's scope contains the validator's.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Part of #20281
Clause-②: yes (narrowing)
Stage 1 (spec) of ruling A (
5904845660) on #20281, as claimed in5913026572:mapping.connectorSource— the target-side pull binding (connector,action, optionalinput/recordsPath/watermark { field, param });rest/openapionly, one-way, no cadence key (ajobowns it), no credential. Declared, not executed: every rowplanned, the containerauthorWarn.syncConfigandfieldMappingsareretiredKey()tombstones pointing at that binding and ajob;DataSyncConfig,SyncStrategy,ConnectorConflictResolution,ConnectorFieldMappingleave whole; D2connector-sync-keys-removed(strip, never writes a mapping); D3connector-sync-keys-retired.packages/spec/docs/SYNC_ARCHITECTURE.mdstops presenting connector sync as delivered (rewritten, 494 to 464 lines).maincontaining 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 (9905e61ca2), viascripts/pm/os-regen-merge.sh.Local, at
674abedfd9: speclocal583/583 files green (17163 passed); spec typecheck exit 0;check:generated15/15 current. The derived-gate union ran atcb54c889b9, before the secondmainmerge: 111 of 114 green.check:platform-checklistis red identically onorigin/main(not this diff);check:dual-build-cjs-loadsandcheck:type-check-debtare NOT MEASURED (they need the whole-repo build CI does). Measurements, ablations and findings are in theos-dev-reportcomment on the card.Acceptance notes
packages/lintstill says themappingledger warns on nothing (a comment inauthoring-rules.tsand a group-C test title inruntime-gate.inert-type-writes.test.ts). The ledger now warns onconnectorSource. The tests stay green because they do not author the key. Carrier: stage 2 of spec(integration): build the connector sync executor thatsyncConfigandfieldMappingsdeclare (14 keys), once and on the mainstream shape #20281.shared/FieldMapping(./sharedFieldMappingSchema) no longer has a carrier in the spec, because its only extender left. No carrier.packages/spec/scripts/build-schemas.tscomments still citeintegration/DataSyncConfigas a live specimen. No carrier.#20281 remains open for stages 2 and 3.
Generated by Claude Code