Repository navigation
Commit b32ee3e
feat(spec)!: approver and decision-output positions are names from the metadata registry (ADR-0131 D4) (#22710)
Part of #22682
Clause-②: yes
Items 1 and 2 of the card: the approver `position` binding and the
decision-output `position` become names, listed from the metadata
registry (ADR-0131 D4). Item 3, the position form's `permissionSets`
widget (`packages/spec/src/identity/position.form.ts`), waits for stage
1 of #15204, so #22682 remains open for it. Claim `6098135473`,
dispatched by the `domain:spec` seat 2.
## What changes
All in `packages/spec/src/automation/approval.zod.ts`.
1. **`ApproverValueBinding` gains a `registry` member**, `{ source:
'registry'; type: string }`: the value is the machine name of an item of
metadata `type`. A picker lists `GET /api/v1/meta/TYPE`, which serves
the code-declared and the environment-authored items alike (ADR-0131
D2), and commits the item's `name`.
2. **`APPROVER_VALUE_BINDINGS.position`** is `{ source: 'registry',
type: 'position' }`. It was `{ source: 'record', object: 'sys_position',
valueField: 'name' }`. The `satisfies` exhaustiveness check, under which
every `ApproverType` member needs a binding, is kept.
3. **`APPROVER_VALUE_SOURCES`** projects a `registry` binding onto the
wire as its own source, so the approval node schema's
`approvers[].value` `xRef.sources.position` reads `{ source: 'registry',
type: 'position' }`. Its type gains `{ source: 'registry'; type: string
}`. `user`, `team` and `department` still project `data`. The docblock
that said the registry "does not hold … `sys_position` ROWS" now says
what each source is for: `data` for a directory row, `registry` for a
catalog item.
4. **`DecisionOutputDef`'s `position` output collects position machine
names.** The docblock said every picker's values were record ids. It now
says each picker kind is sourced and committed exactly like the approver
type of the same name, and the `type` and `multiple` `.describe()`
strings say what each picker commits. The `resolveAs` and node-level
`decisionOutputs` docblocks say the same. The generated reference
`content/docs/references/automation/approval.mdx` is regenerated
(`gen:docs`, the one artifact `check:generated` named).
Departures from the suggested route, both deliberate:
- **No `valueField` on the `registry` member.** A catalog item has no
row id; its name is the only reference it has (ADR-0131 D4), so a
`valueField` could only ever hold `name`, a key with nothing to choose.
- **`type` is `string`, not `MetadataType`.** Importing the kernel's
type, even type-only, pulled `kernel/metadata-plugin.zod.ts` and about
25 transitive modules into the generated skill reference index
(`skills/objectstack-automation/references/_index.md`, a governed
`skills/**` path). The import is dropped, and `approval.test.ts` holds
every registry binding's `type` to `MetadataTypeSchema` instead.
The runtime does not change. `plugin-approvals` routes a position
approver, an escalation target and a `resolveAs: 'position'` value by
filtering `sys_user_position.position` by name, and none of it reads the
binding.
## Premises, re-verified on `origin/main` `243dd3c625`
- `approval.zod.ts:206` was `position: { source: 'record', object:
'sys_position', valueField: 'name' }`. `APPROVER_VALUE_SOURCES` (`:238`)
projected `record` to `{ source: 'data', object, valueField }`.
`DecisionOutputDef` (`:576`) read "Picker values are record ids".
`approval.test.ts:152`, `:193` and `:210` pinned those shapes.
- **No registry binding kind existed.** The union was `record | enum |
auto | trigger-field | expression | unsupported`. `git grep` for
`APPROVER_VALUE_BINDINGS`, `APPROVER_VALUE_SOURCES` and
`ApproverValueBinding` outside `packages/spec` finds one code reader,
`packages/lint/src/validate-approval-approvers.ts:451`, which reads only
`source === 'unsupported'` and is unaffected. The only registry-picker
spelling elsewhere in the spec is the form-widget `ref:TYPE` vocabulary
(`ref:object`, `ref:dataset`, `ref:component`), which is a widget key in
`*.form.ts` and not a value binding. It is item 3's layer, so no second
vocabulary is introduced here.
- **Item 2's values are already names at run time.** `decideNode` hands
outputs to the flow verbatim (`approval-service.ts` `acceptedOutputs`).
The one reader that interprets a position value is an `expression`
approver with `resolveAs: 'position'`, and that calls
`expandPositionUsers(key)`, which filters `sys_user_position` by
`position: NAME` (`approval-service.ts:2686`). A `sys_position` id would
route the request to nobody. So item 2 is a contract and docblock
correction with no value-shape change, and `plugin-approvals` is
untouched.
## What objectui at the pinned `.objectui-sha` does with this
Read at objectui `20c6d351ad74` (the pin) with `git show`.
- **objectui bundles `@objectstack/spec@17.7.0` from npm.** Its lockfile
resolves that version, and `packages/app-shell` and `packages/fields`
declare `^17.6.0`. This change ships on the v18 prerelease line, which
`^17` never resolves. So every reader that imports the constant still
sees the old binding: `approverIdentity.ts`
(`APPROVER_DIRECTORY_BINDINGS`, read by `useApproverDirectory`),
`FlowReferenceField.tsx` (`KIND_TO_RECORD_LOOKUP`) and
`decisionOutputParams.ts` (`OUTPUT_LOOKUP_OBJECTS`, its own hand-written
table).
- **The one reader that takes the binding off the wire** is the flow
designer, which reads `xRef.sources` from the approval node schema the
server publishes. `sourceOf` in `json-schema-to-fields.ts` accepts only
`data`, `enum`, `auto`, `trigger-field`, `expression` and `unsupported`.
It drops any other entry by design ("A dropped entry simply leaves the
consumer on its local fallback"). `sources.position` is therefore
absent, and `recordLookupFor` falls back to the bundled
`KIND_TO_RECORD_LOOKUP.position`, a `sys_position` data lookup that
commits `name`. That is today's control exactly: no throw and no
free-text box, for as long as `sys_position` rows exist.
- **If objectui moves to a spec carrying this change without the C9
follow-up**, `position` drops out of both spec-derived tables, because
both keep only `source === 'data'` and skip anything else without
throwing. The designer's position cell becomes a combobox with no
suggestions, which is the free-text degradation the spec's comment
records. `formatIdentity` stops prettifying `position:sales_manager` and
middle-truncates it to `positi…ager`, and `useApproverDirectory` stops
resolving labels and holders for position seats. Nothing throws.
- **Ordering this implies:** this PR does not need to wait for objectui.
The objectui follow-up (objectui#7611 C9) has to land in or before the
objectui PR that moves objectui's spec dependency to the v18 line, and
before #15204's stage 8 retires `sys_position`, because the pin's
fallback lookup reads that object. The seat decides.
- Today's objectui decision dialog builds the position output as a
`lookup` on `sys_position` with no `idField`, so it commits the row id.
Under the corrected contract that id routes a downstream `resolveAs:
'position'` approver to nobody. That reader is in the C9 follow-up's
census (`decisionOutputParams`). This is a code reading, not a
reproduction.
## Semver and the migration ledger
- `Clause-②: yes`. The published `ApproverValueBinding` and the
`APPROVER_VALUE_SOURCES` type each gain a member, and the `xRef.sources`
payload gains the key `type`.
- **BREAKING**, named in the changeset: the published
`APPROVER_VALUE_SOURCES.position` entry changes shape for an existing
reader. A renderer that keys on `source === 'data'`, as objectui's
derived tables do, loses `position` until it reads `registry`. The
changeset is `minor` on `@objectstack/spec`. Pre mode is on (tag
`next`), and breaking changes ship as `minor` under the launch-window
convention, as the stock does.
- ADR-0087 disposition: `not-required (no-migration-prescription)`. No
authorable key, export or stored value moves. A position approver's
value and a position decision output were names at run time and stay
names, so `objectstack migrate meta` has nothing to rewrite.
- `api-surface/` and `export-origins/` are unchanged, because no export
is added, removed or renamed. `check:api-surface` and
`check:export-origins` pass.
## Tests
The final head is `d4ca8f4762`.
Every run below was taken on that head, after `origin/main` `9646991283`
was merged in. Builds and tests ran through
`scripts/pm/os-verify-lock.sh`, and each reading is the verdict line it
printed.
- `pnpm --filter @objectstack/spec build`, then `typecheck` (`tsc
--noEmit`, the scripts typecheck and the test-layer typecheck): exit 0.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`, the whole local suite: **642 test files and 19,161
tests passed, 1 todo.**
- `src/automation/approval.test.ts`: 66 of 66 passed. Changed pins:
position binds `{ source: 'registry', type: 'position' }`, and every
registry binding's `type` parses under `MetadataTypeSchema`. No binding
names `sys_position`. `APPROVER_VALUE_SOURCES.position` is the registry
source, and `registry` belongs to `position` alone. The published JSON
schema's `xRef.sources.position` is the registry source. Every typed
decision-output picker kind is an approver type bound to `record` or
`registry`.
- **Consumers, the downstream direction.** The
`@objectstack/plugin-approvals` dependency closure was built with turbo
(31 of 31 tasks). `@objectstack/lint`'s full suite passed: 135 files,
6,231 tests. Its reader of the bindings needs no change, and the
existing "accepts the position approver type … silently" pin holds. The
`plugin-approvals` read covered `approval-service.test.ts`,
`approval-node.test.ts` and `approver-address-readers.test.ts`: 3 files
and 378 tests passed. None of them was edited.
- **Ablation**, run through `scripts/ablation-replace.mjs` in wrap mode
on the committed head. The anchor `position: { source: 'registry', type:
'position' },` went from 1 to 0, and the record-binding replacement went
from 0 to 1. The blob moved from `ba3680f2ddef` to `5195b44a4161`.
`approval.test.ts` then went **red, 3 failed and 63 passed**, in the
expected direction: the binding pin, the wire-source pin and the
published-schema pin. The tool restored the file: its blob equals HEAD's
`ba3680f2ddef`, and `git diff HEAD` is empty. A separate `git
hash-object` check agrees.
- **Gates.** `node scripts/pm/dispatch-gates.mjs` (no paths) derives 109
commands on this head. `--ran` with recorded exit codes reconciles **109
derived: 108 run, 1 NOT MEASURED, 0 unrun**.
- Three gates needed a second run. `check:docs-transcript-drift` had
refused for `PREREQUISITE NOT MET`, with lint not built yet.
`check:query-options-erasure` and `check:type-check-debt` had hit a 300
s runner timeout. All three exit 0 on the second run.
- `check:skill-examples` and `check:dts-closure` had read `client-react`
and `verify` dists that the shared turbo cache replayed without
declarations. Both exit 0 after a direct rebuild of those two packages,
which this diff does not touch.
- `check:generated` reports all 15 artifacts up to date.
`check:adr-0087-registration` reports 1 declared-breaking changeset with
a disposition. `check-changeset-no-major` exits 0.
- **NOT MEASURED: `check:dual-build-cjs-loads`.** It refused with
`PREREQUISITE NOT MET` (exit 3) because it needs every package built
(`packages/plugins/organizations/dist` is absent). A repo-wide `pnpm
build` is CI's run. This diff changes no build config, `exports` or
package manifest.
## Acceptance notes
- **The two sentences this change makes false are rewritten in this PR**
(seat answer 6100715923, claim surface amended):
`examples/app-showcase/src/automation/flows/approver-bindings.flow.ts:9-17`
(position is a registry binding committed by machine name) and
`content/docs/automation/approvals.mdx:336-339` (position
decision-output picker values are machine names; `department` / `team`
keep record ids).
- `ApprovalEscalationSchema.escalateTo` carries `xRef: { kind:
'position' }` with no `sources`. objectui resolves its picker through
the same spec-derived fallback, so it follows the binding with the C9
follow-up. No spec change is needed there.
- `ApproverValueBinding`'s `record` member keeps `valueField: 'id' |
'name'`, although no record binding commits `name` now. Narrowing a
published type for no reader would be churn.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b6eb25f commit b32ee3e
6 files changed
Lines changed: 188 additions & 59 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- examples/app-showcase/src/automation/flows
- packages/spec/src/automation
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
333 | 333 | | |
334 | 334 | | |
335 | 335 | | |
336 | | - | |
337 | | - | |
338 | | - | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
339 | 340 | | |
340 | 341 | | |
341 | 342 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
109 | | - | |
| 108 | + | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
154 | | - | |
| 153 | + | |
| 154 | + | |
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
15 | 18 | | |
16 | 19 | | |
17 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
147 | 148 | | |
148 | 149 | | |
149 | 150 | | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
150 | 154 | | |
151 | | - | |
152 | | - | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
153 | 170 | | |
154 | 171 | | |
155 | 172 | | |
| |||
189 | 206 | | |
190 | 207 | | |
191 | 208 | | |
192 | | - | |
193 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
194 | 220 | | |
195 | 221 | | |
196 | 222 | | |
| |||
207 | 233 | | |
208 | 234 | | |
209 | 235 | | |
210 | | - | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
211 | 256 | | |
212 | 257 | | |
213 | 258 | | |
| |||
0 commit comments