Skip to content

feat(spec)!: connector-attached sync leaves the connector — syncConfig / fieldMappings retired, mapping gains the connectorSource pull binding (#20281 stage 1) - #20903

Merged
os-justin merged 10 commits into
mainfrom
claude/issue-20281-mapping-source-binding
Sep 30, 2026
Merged

os-justin merged 10 commits into
mainfrom
claude/issue-20281-mapping-source-binding

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Part of #20281
Clause-②: yes (narrowing)

Stage 1 (spec) of ruling A (5904845660) on #20281, as claimed in 5913026572:

  • mapping.connectorSource — the target-side pull binding (connector, action, optional input / recordsPath / watermark { field, param }); rest / openapi only, one-way, no cadence key (a job owns it), no credential. Declared, not executed: every row planned, the container authorWarn.
  • The 14 connector keys retire: syncConfig and fieldMappings are retiredKey() tombstones pointing at that binding and a job; DataSyncConfig, SyncStrategy, ConnectorConflictResolution, ConnectorFieldMapping leave whole; D2 connector-sync-keys-removed (strip, never writes a mapping); D3 connector-sync-keys-retired.
  • packages/spec/docs/SYNC_ARCHITECTURE.md stops presenting connector sync as delivered (rewritten, 494 to 464 lines).
  • The serial constraint held: the two registry hunks were written only after merging a main containing 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), via scripts/pm/os-regen-merge.sh.

Local, at 674abedfd9: spec local 583/583 files green (17163 passed); spec typecheck exit 0; check:generated 15/15 current. The derived-gate union ran at cb54c889b9, before the second main merge: 111 of 114 green. check:platform-checklist is red identically on origin/main (not this diff); check:dual-build-cjs-loads and check:type-check-debt are NOT MEASURED (they need the whole-repo build CI does). Measurements, ablations and findings are in the os-dev-report comment on the card.

Acceptance notes

#20281 remains open for stages 2 and 3.


Generated by Claude Code

…orSource binding (#20281 stage 1)

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>
…pping-source-binding

# Conflicts:
#	packages/spec/liveness/README.md
#	packages/spec/src/type-alias-convention.pin.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 49 documentable anchor(s). ⚠️ 23 changed file(s) yielded no anchor (packages/spec/api-surface/integration.json, packages/spec/authorable-defaults/integration.json, packages/spec/authorable-surface/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json da93a8b3e0bfa0ac5d4d68059b419435eb7c8371.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 23 changed file(s) yielded no anchor (packages/spec/api-surface/integration.json, packages/spec/authorable-defaults/integration.json, packages/spec/authorable-surface/data.json, …) — pages documenting those are invisible to this run
  • 15 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json da93a8b3e0bfa0ac5d4d68059b419435eb7c8371 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f3801fbee5d8555bebcd4a4d16fd10c6eae0185d — the merge of head 674abedfd9b5a7b139cb13434673ea1f92c7947e into base da93a8b3e0bfa0ac5d4d68059b419435eb7c8371, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs da93a8b3e0bfa0ac5d4d68059b419435eb7c8371 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 674abedfd9b5a7b139cb13434673ea1f92c7947e
Local-runs: none

Read: card #20281 (body + all six comments: triage 5859569880, decision request 5903692970, ruling 5904845660, claim 5913026572, dev report 5916207829, seat ruling 5916259513); PR #20903 body, 51-file list, and the net diff of 674abedfd9 against its merge-base with main (33b6e8bece, 1960/1130); the 46 check-runs on the head; git show / git grep / git log on the fetched refs. Nothing built, run or re-run.

① Derived judgments

A. Accept-set and public-surface changes the diff implies — each judged

  • Connector.syncConfig, Connector.fieldMappings, and the same two keys on DeclarativeConnectorEntry: accepted-and-stored → refused at parse (invalid_type from retiredKey(), path = the key, message = the prescription) and in tsc. Both keys sit on the private ConnectorBaseSchema so both carriers, stack.connectors[], the /meta/connector door (getMetadataTypeSchema('connector')) and AutomationEngine.registerConnector(def: Connector) (which runs ConnectorSchema.parse(def), engine.ts:3859) all refuse. Right. Every door is pinned with a lit control in connector-sync-retirement.test.ts.
  • DataSyncConfigSchema, SyncStrategySchema, ConnectorConflictResolutionSchema, ConnectorFieldMappingSchema and their DataSyncConfig, DataSyncConfigParsed, SyncStrategy, ConnectorConflictResolution, ConnectorFieldMapping, ConnectorFieldMappingParsed aliases leave @objectstack/spec/integration (api-surface: exactly those 10 lines; export-origins, declaration-map and the JSON-schema manifest agree; RETIRED_DEFS_BY_MAJOR[18] gains the four defs). Right — an exported value schema with no carrier reads as a capability, the same rule the tree already applies to ConnectorHealth / ConnectorTrigger.
  • Mapping.connectorSource added: a strictObject (connector name-pattern = ConnectorSchema.name's, action non-empty, optional input record, optional recordsPath, optional watermark { field, param } with both halves required), no .default() anywhere, aliases for the near-miss spellings, a guidance table refusing schedule / cron / interval (points at job and Job.schedule), deleteMode / conflictResolution / direction (version 1 is a one-way pull writing through mode / upsertKey), strategy (full vs incremental is whether watermark is set) and auth / credentialRef (the connector instance holds the credential). Right, and strict-object.ts supports guidance (line 155). The key name avoids Mapping's existing source → sourceFormat alias (mapping.zod.ts aliases block, verified); fieldMappings stays a string alias of fieldMapping on the mapping and the walk's matcher treats it as such. authorable-surface/data.json gains data/Mapping:connectorSource. Right.
  • RENAMED_DEFS loses integration/FieldMapping → integration/ConnectorFieldMapping and integration/ConflictResolution → integration/ConnectorConflictResolution: right and forced — checkRenameTable refuses a rename whose target the build no longer emits (the integration/RateLimitConfig precedent in the same file).
  • authorable-defaults/integration.json loses the 8 rows for DataSyncConfig / ConnectorFieldMapping defaults: right — no residue is owed because neither retired KEY carried a default of its own (the nested defaults leave with their defs).
  • Liveness: connector.json collapses syncConfig (8 drilled rows) and fieldMappings (7, transform tombstone included) into two leaf dead tombstone rows (the health precedent; the gate refuses children on a non-container); mapping.json gains connectorSource planned + authorWarn + authorHint with 7 drilled planned rows; state-counts connector 31/0/0/23/1/55 → 31/0/0/10/1/42, mapping 14/0/0/0/0/14 → 14/0/0/0/6/20. Right; Spec property liveness is green and the counts are pinned by the new test. The README connector row's dead partition (metadata + nine tombstones = 10) and its nine-tombstone list are re-derived, not carried. Right.
  • cron-typed-positions-retirement.test.ts: integration/DataSyncConfig:schedule moves from the strip pins to LEFT_WITH_THEIR_DEFS; the 2026-09-10 直接删 ruling on the KEY is preserved (still no key-level registration, conversion or D3 names schedule; the D2 test pins that a stored cadence leaves with the container in the container's own notice, from: 'syncConfig'). The manifest-reachable path becomes louder (refusal at the container) rather than silent. Right — this is a consequence of retiring the container, not a reversal of the ruling.
  • type-alias-convention.pin.test.ts 780 → 778 (two M78 enum pins leave with their schemas; Spot 2 re-pointed at RetryConfig): right; build-schemas-check-mode.test.ts fixtures re-pointed at integration/RetryConfig and data/FieldMapping (same root, same depth, 4 keys carried so the rename fixture stays non-vacuous): right; test-typecheck-debt.json shrinks by one (SyncStrategySchema unused-import row): right.
  • Reference docs regenerated (index 1525 → 1521 schemas, integration page 16 → 12, mapping.mdx gains the connectorSource nested-shape table): right, and the head's last commit is the regeneration on the merged tree.

B. The binding against each execution parameter of ruling 5904845660

  • No cadence key: none declared; schedule / cron / interval refused by guidance naming job / Job.schedule (system/job.zod.ts:192 carries schedule with expression). Met.
  • No credential contract: none declared; auth / credentialRef refused, pointing at the instance's ADR-0097 auth. Met.
  • rest / openapi only: stated in connector's describe text, the ledger row and every prescription. Not structurally enforced — the schema takes a name and nothing resolves it to a provider (a cross-item check belongs to stage ②'s reader or a lint). Met at spec level; see the "validated at authoring" note below.
  • One-way pull: no direction key; refused by guidance. Met.
  • Full or timestamp-incremental: watermark optional, no strategy switch; field + param structurally paired. Met. field / param map onto what the rest request action returns and takes: packages/connectors/connector-rest/src/rest-connector.ts:172 builds baseUrl + path + query, returns { status, ok, body } (line 68 / 200 in openapi), and the openapi provider keys actions by operationId (openapi-connector.ts:209) — so action: 'request' on rest, an operation key on openapi, and recordsPath defaulting to body are all true of the tree.
  • Declared not executed: every row planned, container authorWarn; lintLivenessProperties is registered with commands: ALL at advisory tier (authoring-rules.ts:1501), walks { type: 'mapping', key: 'mappings' } (lint-liveness-properties.ts:510), and validate.ts:461 / compile.ts:494 (the build command) run it — so "os validate / os build warn when it is authored" is true on main the moment this PR lands.
  • Tombstones + one D2 + one D3 in the spec stage: connector-sync-keys-removed (toMajor 18, retiredFromLoadPath, order 50 — unique in MAJOR_18_CONVERSIONS; rationale fragment order 52 — unique) strips via mapCollection / stripKeys, one notice per key, never writes a mapping; fixture 3 connectors / 3 notices; the rehydration-seam pin asserts the row comes back key-for-key minus the two keys and parses on the tombstoned door; a second replay yields 0 notices and the same reference. Lossless and idempotent, pinned. connector-sync-keys-retired is the family's one D3 entry, naming the D2 id, connectorSource and job. Met.
  • retiredAfter: '17.5.0' while every sibling major-18 entry says 17.4.0: right — types.ts:262 defines it as the package's version label at the moment the retirement lands, and packages/spec/package.json reads 17.5.0 on both base and head.
  • Registry hunks additive against main after 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: git diff numstat is +141/−0 (migrations) and +94/−0 (conversions); view-item-options-bag-refused survives at registry.ts:18495; 9905e61ca2 is an ancestor of the merge 9eec71f925 (main side ca5408c629), and no branch commit before that merge touches either registry — the hunks first appear in 8e0f4886c5. The serial constraint held.
  • SYNC_ARCHITECTURE.md 494 → 464 lines, and no sentence on the head file presents connector sync as delivered: the L1 and L2 "what to use instead" bullets, the L3 feature list (now a ❌ line), the new "Data sync is defined on the target" section, both example blocks, the decision matrix rows, Pattern 1 / 2, and the migration section all say declared / not yet executed, and the doc-compile pin (connector-author-shape.test.ts) compiles both typescript blocks. The After example's type: 'api' is a ConnectorType member (connector.zod.ts:737), provider: 'rest' + providerConfig.baseUrl is what the rest factory reads, and recordsPath: 'body.items' is coherent with the { status, ok, body } return.

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 RETIRED_DEFS and four RETIRED_KEYS entry comments, the D2 docblock and summary, the D3 surface / replacement / reason / acceptanceCriteria, the STEP18 rationale fragment, the connectorSource TSDoc and every .describe(), the guidance strings, the two ledger notes and the README rows, the rewritten SYNC_ARCHITECTURE.md, the regenerated connector.mdx / mapping.mdx text, the changeset, and the PR body. All state facts the tree holds at this head; the measurements they cite (two comment hits for syncConfig outside packages/spec, zero for fieldMappings, the automation service's declared-connector item carrying neither key, connectorInstanceSignature at plugin.ts:290, retryConfig as the lit control read by connector-fetch-policy.ts, hard deletes only per object.zod.ts's softDelete guidance, runImport at packages/rest/src/import-runner.ts:374) check out. "Removed in @objectstack/spec 17" and os migrate meta --from 17 follow the five sibling tombstones on the same schema (the package is 17.5.0; the next 17.x release is the first to refuse; applyMetaMigrations(from, to) runs the step-18 chain from 17). Four sentences carry imprecision, none blocking:

  1. "the key's input type is never" (tombstone TSDoc, changeset, sync-retirement.test.ts title) — retiredKey() is z.never().optional(), so the measured tsc diagnostic is "not assignable to type 'undefined'", which connector-author-shape.test.ts pins and says so. House shorthand used verbatim by the five existing tombstones in the same file; true as shorthand on main now.
  2. connector.mdx (generated from the schema header): 'The "REMOVED: connector-attached sync" section below records the measurement.' — true in connector.zod.ts, where the block is a comment; on the generated page there is no such section, so "below" dangles. The neighbouring precedent sentence names the file instead. Cosmetic; fix at the next header touch by naming the file.
  3. D3 acceptanceCriteria: "a mapping whose connectorSource names a rest or openapi connector instance, validated at authoring" — MappingSchema validates the binding's shape at authoring; nothing at this head checks that the named connector exists or that its provider is rest / openapi. True of the shape today; true of the provider restriction only once stage ② (or a lint) resolves the name. An upgrading author should read "validated" as shape validation.
  4. SYNC_ARCHITECTURE.md Best Practices, two untouched bullets — "Test authentication and sync flows thoroughly" and "Document field mappings and business logic" — read stale under an L3 that now carries no sync and no field mapping. Not a delivery claim; carry to the next docs touch.

One more reading note: the code comments and the changeset cite "measured on origin/main before the removal" without a sha; the reading of record with its tree (793fb8390e) is in the two ledger notes and the dev report. The AGENTS.md count-plus-tree rule is satisfied by the instrument, not by the comment text.

② Semver level

.changeset/20281-connector-sync-moved-to-mapping.md: '@objectstack/spec': minor with a BREAKING banner, the FROM → TO table, the one-line fix, the os migrate meta --from 17 sentence, a "runtime behaviour unchanged" note, the out-of-repo consumer caveat, and the adr-0087 registered marker naming connector-sync-keys-removed, connector-sync-keys-retired (the form check-adr-0087-registration.mjs:62 reads). That matches what the diff publishes: ten exports leave ./integration, two authorable keys become tombstones on both carriers, four defs leave the manifest, and one authorable key is added on mapping. The level follows the tree's precedent for a breaking spec narrowing (20282-analytics-cube-format-granularities-enforced.md is likewise '@objectstack/spec': minor with BREAKING) and the dispatch's own instruction. PR title carries feat(spec)!:; the PR body and the changeset carry the same declaration. Narrowing dominates the added key.

Clause-②: yes (narrowing)

③ Boundary flags

Dev deviations (5916207829):

  • Registry hunks applied to the working tree before 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 landed, for measurement only, restored with git checkout HEAD, never committed or pushed — verified by history: no branch commit before the merge 9eec71f925 touches either registry, and the hunks first appear in 8e0f4886c5 after that merge. Accepted as a measurement.
  • One single-file vitest run outside os-verify-lock — process discipline; the check-runs are the verdict. Accepted.
  • File surface extended beyond the claim's list, declared instead of stopped: scripts/lib/renamed-defs.ts (the decay guard fails the build otherwise), two barrel comments, nine pins that count or enumerate what moved, vitest.repo-tests.json, test-typecheck-debt.json (shrink). Every edit is inside packages/spec and forced by the move; the claim's "every pin that counts or enumerates what moves" covers the pins. Accepted; concur with the seat ruling.

Open questions (each answered):

  1. Superseded major-17 fixture — A, concur. excludeConversionIds is a pre-existing harness option (conversions/apply.ts:116), so the test-local SUPERSEDED_FIXTURES entry changes no policy; the excluded leg keeps the released fixture's contract byte for byte, the included leg asserts the retiring entry fires and produces exactly after minus the container, a later-major guard is asserted, and both ids are checked against ALL_CONVERSIONS. B would weaken cross-major interference detection for every fixture; C is impossible (published id).
  2. Pagination undeclared — A, concur. Stage ② measures the target APIs; its stop valve trips if paging is needed; the minimal shape the claim asked for stays minimal.
  3. Acting organization for a job-driven pull — A, concur. Cadence and identity belong to stage ③; a key nothing reads must not land on the binding.

Out-of-scope findings (each answered):

  1. packages/lint comment and test title saying the mapping ledger warns on nothing — stage ② carrier; tests stay green because the fixtures do not author the key. Concur.
  2. shared/FieldMapping (./shared FieldMappingSchema) now has no extender and no carrier — the seat left it "no carrier". Escalated as a recommendation, not a block: the reasoning this PR applies to its four defs ("an exported value schema with no carrier reads as a capability") applies here too, so it should get an ADR-0049 follow-up card rather than remain a noted dead export. Outside this claim's file surface, and correctly placed in the PR's acceptance notes per Prime Directive chore: version packages #10.
  3. packages/spec/scripts/build-schemas.ts comments at 1371 and 1627 still cite integration/DataSyncConfig as a specimen — verified comment-only, no code path names the def. Noted, not filed. Concur.
  4. skills/objectstack-upgrade/references/examples-upgrade.md (Tier H) walks a 16-era fieldMappings[].transform source and names a connector sync run that never existed — the seat carries it into a Tier H docs PR. Concur; not this PR's to touch.
  5. check:platform-checklist red on origin/main — the watchdog's card. Concur.

Two review-side flags for the seat, neither blocking:

  • Console Pin Gate was skipped by ci.yml's path filter, so the Post-Task Checklist Add Changesets and GitHub Actions automation #4 question (does the pinned sibling import what is removed) is answered only by the dev's census at pin db11afd496 (no retired name imported; the only hits are comments, CHANGELOGs and an unrelated useOffline syncConfig). The sibling repo is outside this review's inputs; the census is accepted as declared.
  • The four text imprecisions in ① C are recorded for the next touch of each file; none makes a sentence false on main when this PR lands.

Check-runs on 674abedfd9, read last, deduped by name keeping the newest started_at: 46 raw → 35 names, all completed, none running. 31 success, 4 skipped (Auto Label, Check PR Size, Console Pin Gate — path filter false; Packed-tarball smoke — opt-in label absent). The seven required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard — are all success; Check Changeset, Spec property liveness, all four Type Check jobs, the three single-writer / claim guards and Check Documentation Links are success. Newest run: Test Core at 2026-09-30T17:31:50Z. Governance: 0 of 51 paths hit the governed register; content/docs/references/** edits are regenerations.

Implemented-by: claude/issue-20281-mapping-source-binding
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-30T17:50Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting.

  • The four text imprecisions: none is a delivery claim. They ride stage ②, which edits these same surfaces: connector.mdx "section below", the D3 "validated at authoring" (shape validation until stage ② resolves the name), and the two stale Best Practices bullets. "input type is never" is the house shorthand the five sibling tombstones use, and it stays.
  • shared/FieldMapping without a carrier is recorded on the card for an ADR-0049 follow-up.
  • Not governed. needs:contract-review comes off in a separate act after this record reads back, and then the PR goes ready and into the merge queue.

Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 30, 2026 17:52
@os-justin
os-justin enabled auto-merge September 30, 2026 17:52
@os-justin
os-justin added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 0efbdc3 Sep 30, 2026
51 checks passed
@os-justin
os-justin deleted the claude/issue-20281-mapping-source-binding branch September 30, 2026 18:24
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants