Repository navigation
Commit ec292cf
Part of #19332
Flight G1b of ruling 5861442317.
Clause-②: no
## What
Sixteen live keys that the field and action schemas declare had no form
row, so an author could reach them only through the Source tab. Each now
has exactly one row. Each row copies a row that a registered form
already has for the same node shape, and the four-locale catalogue rows
are in the same PR. This is the `field.*` / `action.*` half of the
ruling's G1; G1a (the object and permission rows) landed as `7db1332f`.
| key | form, section | control | the row it copies | gate |
|:--|:--|:--|:--|:--|
| `field.visibleWhen` | `field.form.ts`, Advanced | `type: 'code'`,
`language: 'expression'` | `object.form.ts:347` `fields.visibleWhen`
(same key, same node) | none |
| `field.readonlyWhen` | `field.form.ts`, Advanced | same |
`object.form.ts:348` | none |
| `field.requiredWhen` | `field.form.ts`, Advanced | same |
`object.form.ts:349` | none |
| `field.lookupFilters` | `field.form.ts`, Configuration | `widget:
'json'`, no inline options | `object.form.ts:253` `fields.lookupFilters`
(same key, same node) | lookup / master_detail |
| `field.lookupColumns` | `field.form.ts`, Configuration | `widget:
'json'` | the same `lookupFilters` row | lookup / master_detail |
| `field.dependsOn` | `field.form.ts`, Configuration | `widget: 'json'`
| the same `lookupFilters` row | lookup, master_detail, select,
multiselect, radio, checkboxes |
| `field.relatedListColumns` | `field.form.ts`, Configuration | `widget:
'string-tags'` | `app.form.ts:102` `requiredPermissions` | lookup /
master_detail |
| `field.accept` | `field.form.ts`, Configuration | `widget:
'string-tags'` | `app.form.ts:102` | the five media types (`maxSize`'s
gate) |
| `field.currencyConfig` | `field.form.ts`, Configuration | `type:
'composite'`: a declared `currencyMode` select (`dynamic` / `fixed`) and
`defaultCurrency` text | `object.form.ts:453` `access` | `currency` |
| `field.storage` | `field.form.ts`, Advanced | `type: 'composite'`: a
declared `notNull` boolean | `object.form.ts:453` `access` | none |
| `field.requiredPermissions` | `field.form.ts`, Advanced | `widget:
'string-tags'` | `app.form.ts:102` | none |
| `action.patch` | `action.form.ts`, Behavior | `widget: 'json'` |
`object.form.ts:424` `validations` | `data.operation == 'update'`
(`undoable`'s gate) |
| `action.description` | `action.form.ts`, Behavior | `widget:
'textarea'` | `page.form.ts:43` `description` (same `I18nLabel` node) |
none |
| `action.errorMessage` | `action.form.ts`, Behavior | plain row |
`action.form.ts` `successMessage` (its twin, same node) | none |
| `action.requiredPermissions` | `action.form.ts`, Placement | `widget:
'string-tags'` | `app.form.ts:102` | none |
| `action.bodyExtra` | `action.form.ts`, Advanced | `widget: 'json'` |
`object.form.ts:424` `validations` | `data.type == 'api'` (`bodyShape`'s
gate) |
The ruling's widget rules, as applied here:
- `lookupColumns` and `dependsOn` are arrays of a union (a field name,
or an object entry). They take `json` and never `string-tags`. The tag
widget is a chip input for strings only, so it cannot show or edit a
stored object entry.
- `accept`, `relatedListColumns` and both `requiredPermissions` are
plain `string[]`, so they take `string-tags`.
- The field-name lists are free text. No field picker is offered:
nothing on a field draft gives a picker a catalogue to read.
- `currencyConfig` is `{currencyMode, defaultCurrency}` today. Its
`precision` key, which the analysis table listed, was removed before
this flight. Both `currencyMode` members are spellable option values.
Every gate is a meaningfulness gate, taken from the runtime reader: the
file-constraint pass for `accept`, the lookup picker and the four option
widgets for `dependsOn`, and the related-list derivation for
`relatedListColumns`. There are two exceptions. The parse refuses
`patch` without `operation: 'update'`. It also refuses `bodyExtra`
beside that operation.
The help text says what the runtime does with each value, including what
absence resolves to. Each claim was checked against its reader:
- `readonlyWhen`: `stripReadonlyWhenFields` (objectql
`validation/rule-validator.ts`) drops an update's change to a locked
field and keeps the stored value. It does not refuse the write.
- `requiredWhen` is a transition gate: `evaluateValidationRules` refuses
a write only when the record complied before it. The parse refuses it
beside `storage.notNull` (`field.zod.ts`, the ADR-0113 check).
- `field.requiredPermissions` is all-of. `foldFieldRequiredPermissions`
(plugin-security) sets a field to unreadable and uneditable unless every
listed capability is held. A `maskingRule` softens the read into a
partial mask.
- `action.requiredPermissions` is all-of too. `actionPermissionError`
(runtime `action-execution.ts`) reports the missing subset.
- `accept`: `assertFileConstraints` (service-storage) re-checks it on
write against the stored `sys_file`.
- `patch`, `bodyExtra`, `description`, `errorMessage` and
`currencyConfig`: the texts restate each key's own schema description.
No schema's accepted input changes, and no export changes. What changes
is the form payload that `getMetaTypes()` serves, and the translation
keys that `os i18n extract` walks.
## Where a misspelt field name is refused (dispatch assumption 2)
The ruling says 「a misspelling is refused loudly at parse」. G1a measured
its two lists as refused at the publish door and by `os validate`. **For
this flight's four lists, no authoring door judges the names**: not the
Zod parse, not the publish door, not `os validate`. So the help text
claims no refusal. From the code:
- No rule in `packages/lint/src` reads `relatedListColumns`,
`lookupColumns` or `dependsOn`.
- `lookupFilters` is walked by the filter-token, empty-combinator and
preset-comparand rules. Those judge values. The preset rule's own header
leaves a mistyped field to the `*-filter-field-unknown` rules, and those
exist only for datasets, dashboards and widgets.
What happens at run time instead. These are code readings; none was run
end to end.
- `relatedListColumns`: the related list puts the authored columns in
its `$select` (objectui `RelatedList.tsx` `selectFields`, at the pin).
The REST read ingress refuses an unknown projection field with 400
`INVALID_FIELD` (`assertProjectionFieldsExist`). NOT MEASURED: whether
objectui's FLS column gate drops an unknown name first.
- `lookupFilters[].field`: the picker lowers it to a `$filter` on the
referenced object. The read ingress refuses an unknown filter field
(`assertFilterFieldsExist`).
- `lookupColumns`: the record-picker dialog draws the column and sends
no projection, so a mistyped name renders an empty column.
- `dependsOn`: `LookupField` gates the picker until each named value is
set. A mistyped name is never set, so the picker stays gated ("select X
first").
## objectui widgets, read at the `.objectui-sha` pin (`f8a9d0fb0`,
source only, no browser)
G1a found that both halves of the ruling's premise hold at the pin:
`string-tags` reads a non-array as an empty list, and `field-multi`
binds nothing on an object draft. This flight reuses that reading. The
faces G1a did not use were read in `SchemaForm.tsx` `resolveFieldFace`
and `widgets.tsx`:
- **`type: 'code'`** is the registered `CodeWidget`. It reads a
non-string value with `String(value)` (`widgets.tsx:3076-3086`). A
stored ADR-0089 envelope would therefore show as `[object Object]`, and
an edit writes a bare string over it. objectui's own
`expression-envelope.ts` says a persisted artifact carries the envelope,
and its `ConditionWidget` comment records this exact failure as fixed
for that widget only. The three predicate rows copy the object
designer's rows, which have the same bound. See Acceptance notes.
- **`widget: 'textarea'`** over an `I18nLabel` is a passthrough hint. A
stored string, or a create, resolves to the string branch and gets a
textarea. A stored locale map resolves to its object branch, which has
no `properties`, so it opens the raw JSON editor. It is never read as
text.
- **`widget: 'json'` over a string-keyed record** (`patch`, `bodyExtra`)
opens the raw JSON editor.
- **`widget: 'json'` over an array of a union** (`lookupColumns`,
`dependsOn`):
- A create, or a list whose first entry is a string, opens the raw JSON
editor.
- A list whose first entry is an object edits as rows.
- In that row face, untouched string rows survive. An edited string row
is spread into an object, which the strict entry schema refuses loudly
at save.
- **Composite sub-rows** are declared, so their labels and help text
reach the catalogues. Schema defaults (`dynamic`, `CNY`) are
placeholders and are never written on mount.
## Residue of the reconciliation gate
I ran the gate's own helper block
(`metadata-form-zod-reconciliation.test.ts` lines 1-808, copied verbatim
into a scratch probe that was never committed; prefix sha256
`91478ba8d05c…`). It ran at the root coordinate over every registered
type, in a second scratch worktree. The probe asserted three controls in
the same run:
- lit: `name` is offered by 17 of 17 forms;
- dark: the fabricated `field.zzFabricated19332G1b` is in no residue;
- a named lit key: `field.accept` before, `field.inlineColumns` after.
| tree | object-rooted residue | per type |
|:--|:--|:--|
| merge base `789b2ae54` | **22** | object 5 · field 12 · action 5 |
| this branch (form files byte-equal to `3190a969`) | **6** | object 5 ·
field 1 · action 0 |
Removed: the sixteen keys above. Added: none. `view` reads 42 on both
trees and is outside this direction (#19330 A). The dispatch predicted
23 → 7. The difference is `action.aria`, which was retired as a
tombstone (`dcd3bcea`) after G1a measured 23, so the residue left is
exactly the six G2 keys.
## Pins this PR moves
- `field-panel-echo-decisions.test.ts`:
- The unwalked composite children go from 5 to 8
(`currencyConfig.currencyMode`, `currencyConfig.defaultCurrency`,
`storage.notNull`).
- The skipped parents become `currencyConfig`, `summaryOperations`,
`storage`.
- Their six string leaves are authored in all three locales, so the echo
rule reads them as translated.
- `object-lifecycle-panel-echo-decisions.test.ts`: the translated-label
positive control goes from 614 to 633, which is nineteen new row labels.
- `packages/lint/src/validate-predicate-path-refs.test.ts` (**outside
the claim's named file surface**; see below): the shipped-form predicate
census goes from 73 to 81, and the literal-comparison count from 53 to
56. This was measured, not inferred. I differenced the shipped corpus
against the merge base by FORM :: FIELD :: SOURCE: eight entries were
added and none removed (`field` accept, currencyConfig, dependsOn,
lookupColumns, lookupFilters, relatedListColumns; `action` patch,
bodyExtra). Three of the eight compare against a quoted literal.
The claim named the two form files, the catalogues, the echo-decision
pins and `.changeset/`. The lint census is a measured population pin
that these rows move mechanically: the #19331 rows moved the same pin.
It went red on this branch at 73 vs 81 and 53 vs 56. So it moves here,
with its reason written beside it. No forbidden file was touched: not
`object.form.ts`, not the reconciliation ledger, and not any `*.zod.ts`.
## Verification
Every test run went through `scripts/pm/os-verify-lock.sh`, and each
reported `VERDICT command-exit 0`.
| run | result |
|:--|:--|
| `pnpm --filter @objectstack/spec test` | Test Files 561 passed, 1
skipped (562) · Tests 16534 passed, 1 skipped, 1 todo (16536). The
skipped file was not identified; the suite's only file-level skips are
the environment `skipIf` guards on the `publish-smoke` script tests |
| `pnpm --filter @objectstack/spec test:repo` | Test Files 35 passed
(35) · Tests 634 passed (634) |
| `pnpm --filter @objectstack/platform-objects test` | Test Files 55
passed (55) · Tests 911 passed (911) |
| `pnpm --filter @objectstack/lint test` | Test Files 115 passed (115) ·
Tests 5314 passed (5314) |
| spec / platform-objects / lint `typecheck` | each exits 0
(`check:test-typecheck` OK: 53/253/140, 1/3/2 and 2/6/2
files/errors/signatures held) |
| cli unit `test/i18n-coverage.test.ts` and
`test/i18n-duplicate-demand.test.ts` (they read the form registry) | 2
files, 27 tests passed |
| metadata-protocol `protocol.meta-types-*` (the three files that read
the registry) | 3 files, 38 tests passed |
| `pnpm check:i18n` | `OK (9 package(s) — all bundles in sync, no
undeclared authoring keys)` |
| `pnpm --filter @objectstack/spec check:generated` | all 15 generated
artifacts up to date |
**Catalogues.** I regenerated them with `node
scripts/check-i18n-bundles.mjs --write`, wrote the 114 translated leaves
(38 en leaves, three locales) by hand, and ran `--write` again. The
second write kept every translated value, and no source-hash row was
left.
**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 86 commands at `169b0cc7`. All 86
ran, and each exit code was written to disk before it was read. `--ran`
reports: 86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN. Two gates first
exited 3 on a prerequisite; each rerun exited 0, and the rerun's code is
the one recorded:
- `check:dual-build-cjs-loads`: three packages had no `dist/`. After
building them it reported `104 published require entry point(s) across
66 package(s) load`.
- `check:type-check-debt`: its prerequisite build failed on a
`plugin-auth` declaration build. I had run a second turbo build in the
same worktree at the same time, and it rewrote `@objectstack/rest`'s
`dist/` mid-flight. Rerun alone, it reported `4 ledger entr(ies)
re-measured … none above its recorded number`.
No ablation was run. This PR adds rows and moves population pins; it
adds no guard. The residue measured before and after is the measurement
of the delta.
`origin/main` is two commits past this branch's base (`50e273fd`,
`40b315b0`). Neither touches a file in this diff, so I did not merge it.
## Acceptance notes
- **The ruling's 「refused loudly at parse」 does not hold for this
flight's four field-name lists.** No authoring door judges their names,
as described above. The rows are still free text, as ruled. A
reference-integrity rule for field-level lists would be the loud door.
It is reported to the seat, not filed here.
- **`type: 'code'` cannot show a stored expression envelope**
(`CodeWidget`, `String(value)`). The flaw is the same on the object
designer's `fields.visibleWhen` / `readonlyWhen` / `requiredWhen` /
`expression` rows and on `hook.condition`. The fix that closes the whole
class is in objectui: `CodeWidget` could read and write through the same
`expressionSource` / `writeExpressionSource` pair `ConditionWidget`
uses. It is reported to the seat, not filed here.
- `lookupColumns` / `dependsOn` in the row face: an edited string entry
in an object-first mixed list is refused loudly at save. This is the
generic objectui repeater, not a silent loss. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e01d347 commit ec292cf
10 files changed
Lines changed: 465 additions & 5 deletions
File tree
- .changeset
- packages
- lint/src
- platform-objects/src/apps/translations
- spec/src
- data
- ui
| 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 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
596 | 596 | | |
597 | 597 | | |
598 | 598 | | |
599 | | - | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
600 | 609 | | |
601 | 610 | | |
602 | 611 | | |
| |||
687 | 696 | | |
688 | 697 | | |
689 | 698 | | |
690 | | - | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
691 | 705 | | |
692 | 706 | | |
693 | 707 | | |
| |||
Lines changed: 76 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
464 | 464 | | |
465 | 465 | | |
466 | 466 | | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
467 | 479 | | |
468 | 480 | | |
469 | 481 | | |
| |||
472 | 484 | | |
473 | 485 | | |
474 | 486 | | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
475 | 491 | | |
476 | 492 | | |
477 | 493 | | |
| |||
538 | 554 | | |
539 | 555 | | |
540 | 556 | | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
541 | 569 | | |
542 | 570 | | |
543 | 571 | | |
544 | 572 | | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
545 | 577 | | |
546 | 578 | | |
547 | 579 | | |
| |||
586 | 618 | | |
587 | 619 | | |
588 | 620 | | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
589 | 629 | | |
590 | 630 | | |
591 | 631 | | |
| |||
602 | 642 | | |
603 | 643 | | |
604 | 644 | | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
605 | 657 | | |
606 | 658 | | |
607 | 659 | | |
608 | 660 | | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
609 | 665 | | |
610 | 666 | | |
611 | 667 | | |
| |||
1535 | 1591 | | |
1536 | 1592 | | |
1537 | 1593 | | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
1538 | 1598 | | |
1539 | 1599 | | |
1540 | 1600 | | |
| |||
1543 | 1603 | | |
1544 | 1604 | | |
1545 | 1605 | | |
| 1606 | + | |
| 1607 | + | |
| 1608 | + | |
| 1609 | + | |
1546 | 1610 | | |
1547 | 1611 | | |
1548 | 1612 | | |
| |||
1551 | 1615 | | |
1552 | 1616 | | |
1553 | 1617 | | |
| 1618 | + | |
| 1619 | + | |
| 1620 | + | |
| 1621 | + | |
1554 | 1622 | | |
1555 | 1623 | | |
1556 | 1624 | | |
| |||
1587 | 1655 | | |
1588 | 1656 | | |
1589 | 1657 | | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
1590 | 1662 | | |
1591 | 1663 | | |
1592 | 1664 | | |
| |||
1602 | 1674 | | |
1603 | 1675 | | |
1604 | 1676 | | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
1605 | 1681 | | |
1606 | 1682 | | |
1607 | 1683 | | |
| |||
0 commit comments