Skip to content

feat(spec): declare placeholder on FieldSchema — Option C ruling on objectui#4676 (#9019) - #9113

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-9019-fieldschema-placeholder
Aug 16, 2026
Merged

feat(spec): declare placeholder on FieldSchema — Option C ruling on objectui#4676 (#9019)#9113
os-zhuang merged 4 commits into
mainfrom
claude/issue-9019-fieldschema-placeholder

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9019

Implements the maintainer ruling of 2026-08-16 (Option C on objectui#4676, 「主线1 3 4:同意」): placeholder becomes a declared optional string key on FieldSchema, with the semantics the four shipped objectui render surfaces already implement (measured in objectui#4676 report comment 5301288148: plugin-form, plugin-detail, app-shell resolveActionParams, apps/console FormPage — feeding the @object-ui/fields widgets).

What changed

  • packages/spec/src/data/field.zod.tsplaceholder declared next to inlineHelpText, .describe() prose carries the three-way distinction (in-input placeholder that disappears on typing, vs inlineHelpText always-visible help beside/under the input, vs description tooltip/developer docs), with a doc comment recording the ruling and the measured consumers.
  • packages/spec/src/data/authoring-key-lint.ts — the FIELD_KEY_GUIDANCE retirement entry ("never a FieldSchema key. Author hint text through inlineHelpText or description.") is REMOVED, replaced by a NOTE comment recording the flip. Note: the prescription prose lived here (folded into FieldSchema's strictObject guidance options via fieldKeyGuidanceAsStrictOptions()), not literally in field.zod.ts as the card assumed — same table region the dispatch warned about, one file over. The authoring-key-lint.test.ts no-rot pin ("no guidance entry names a key the schema now declares") makes leaving the entry a red build, so the flip is gate-enforced, not convention.
  • packages/spec/src/data/object.form.ts + field.form.ts — the "placeholder removed: never a FieldSchema key" tombstone comment in object.form.ts is replaced with the actual form field entry, and the full per-field editor (field.form.ts) offers the key too. Bounded same-class completion, named here per the in-place exemption: the object.form.ts comment was mandated prose-flip surface; the field.form.ts row makes the declared key authorable in the editor the reconciliation test names as "where the long tail of FieldSchema is authored". Both entries are validated by metadata-form-zod-reconciliation.test.ts (form-only direction).
  • packages/spec/liveness/field.jsonlive row with evidenceScope: "cross-repo" citing the measured objectui read points @9ce096fb0 (the ledger gate demands a row for every newly authorable property; evidence format follows the existing precision cross-repo row). state-counts.md regenerated.
  • Generated: authorable-surface/data.json gains data/Field:placeholder (base anchor lag is the allowed informational state); content/docs/references/data/field.mdx regenerated wholesale via gen:docs — the new row renders the distinction.
  • Tests: field.test.ts gains an accept-side describe block — parse + round-trip, coexistence of all three hint keys, optional-stays-absent, non-string rejection, no type-restriction (flat like scale/min), unknown-key strictness undisturbed, .describe() prose pin via JSON Schema, and the object-document shape that used to 422 on PUT /api/v1/meta/object/:name.
  • Changeset: minor on @objectstack/spec (accept-set expansion on a published surface), with the ADR-0087 not-required annotation (purely additive).

Reverse verification (direction: rejection flips to acceptance)

Pre-change probe on unmodified origin/main @9bf110fb6, built dist: {name,label,type:'text'} control SUCCESS; +placeholder FAIL unrecognized_keys with the exact "never a FieldSchema key" prescription; +inlineHelpText / +description SUCCESS. Post-change probe on the rebuilt dist: +placeholder SUCCESS and round-trips; non-string placeholder still rejects. This is the expected direction for an accept-set expansion — the old reject-with-prose pin is gone (guidance entry removed, enforced by the no-rot gate) and the accept side is pinned in field.test.ts.

#9088 same-file churn

#9088 (currency guidance in the same field.zod.ts guidance region) landed mid-task as predicted; origin/main re-merged after it (merge commit 7cb089f37), clean merge, both semantics verified present: the currency guidance entry survives in the guidance: block and the placeholder declaration stands. Full spec suite re-run green after the merge.

Verification (all at final HEAD 7cb089f37, after the merge commit)

  • @objectstack/spec full suite: 406 files, 10771 tests, all passed. Typecheck green.
  • Consumer sweep (direction: downstream consumers of @objectstack/spec, named explicitly): rest 2001 ✓, objectql 3730 ✓, lint 2065 ✓, metadata 603 ✓, metadata-core 162 ✓, metadata-fs 51 ✓, metadata-protocol 1565 ✓ — zero fixture flips needed (no consumer fixture pinned the field-level placeholder rejection; searched).
  • Gate union re-derived via node scripts/pm/dispatch-gates.mjs against the actual diff and run in full: changeset-gate-self-tests, cross-package-test-inputs (both forms), docs-audit-scope, docs-redirects, merge-driver, objectui-changeset, quick-reference-counts, role-word, spec-parsed-alias, type-source-resolution, nul-bytes, adr-0087-registration, changeset-no-major, empty-changeset, dev-prereqs, doc-formula-expressions, query-options-erasure, engine-double-contract, where-matcher, type-check-coverage, type-check-debt (workspace closure built via turbo first, 70/70), spec check:api-surface / check:authorable-surface / check:docs / check:generated / check:liveness / check:export-origins — all EXIT=0. Beyond the prompt's list, the derivation added the changeset/docs families (changeset-gate-self-tests, objectui-changeset, adr-0087, no-major, empty-changeset, docs-audit-scope, docs-redirects, quick-reference-counts, role-word) — all run, all green.
  • Example apps validate: app-crm, app-showcase, app-todo — all EXIT=0 (pre-existing draft-flow warnings only).

objectui side per the ruling: zero code change expected there; objectui#4676 unblocks on the release + pin bump.

Generated by Claude Code


Generated by Claude Code

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 16, 2026
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec.

106 hand-written doc(s) reference the affected code — list omitted above 15 rows. Re-derive: node scripts/docs-audit/affected-docs.mjs --json origin/main.

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

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 16, 2026 11:38
@os-zhuang
os-zhuang added this pull request to the merge queueAug 16, 2026
Merged via the queue into main with commit c5ac5e4Aug 16, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9019-fieldschema-placeholder branch August 16, 2026 11:56
os-zhuang pushed a commit that referenced this pull request Aug 16, 2026
os-project-manager pushed a commit that referenced this pull request Aug 17, 2026
…upling
walkMetadataForms (packages/cli/src/utils/i18n-extract.ts) emits the
metadataForms i18n surface from two producers: METADATA_FORM_REGISTRY (the
17 *.form.ts modules, already covered by the form-module convention entry
in dispatch-gates.mjs) and DEFAULT_METADATA_TYPE_REGISTRY plus the
METADATA_FORM_REGISTRY module itself (packages/spec/src/kernel/
metadata-plugin.zod.ts, packages/spec/src/system/metadata-form-registry.ts).
Editing either of the latter two moves the same platform-objects bundles
PR #9113 had to regenerate, and derived nothing from dispatch-gates.
Both are exactly two known files, not a runtime-enumerated population, so
this closes the gap the same way SURFACE_MODULE and
check-type-check-coverage.mjs's ROOT_PROGRAM_COUPLED_SCRIPT already do:
two bare module-body path-literal constants declared directly in
check-i18n-bundles.mjs (the script check:i18n resolves to), read by the
ordinary path-literal derivation with no new CHANGE_KIND_GATES entry.
Pinned live in dispatch-gates.mjs's own self-test (existsSync + a matched
classifyEntry verdict for each), so a renamed or moved registry module
reddens that self-test instead of the derivation silently going blind on
this edge again.
Fixes#9144
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ts + keepHead/keepTail, enforced on the FieldMasker channel (objectstack-ai#8993) (objectstack-ai#9128)
* feat(spec,plugin-security): partial field masking — maskingRule presets + keepHead/keepTail, enforced in FieldMasker (objectstack-ai#8993)
Re-introduces the 2026-06-pruned maskingRule key WITH its runtime consumer in
the same PR (ADR-0049 declare = enforce; maintainer ruling 2026-08-16 Option A):
- spec: FieldMaskingRuleSchema — closed preset enum (phone/id_card/
bank_account/email/name) + keepHead/keepTail escape hatch; guidance
tombstone flipped (the objectstack-ai#9019 placeholder precedent); liveness row live with
evidence; authoring forms; regenerated artifacts.
- plugin-security: maskFieldValue (deterministic, length-preserving,
idempotent); maskResults REPLACES masked-for-caller fields instead of
deleting (requiredPermissions is the unmask gate — same ADR-0066 D3
evaluation; explicit permission-set denies still delete); predicate +
aggregate guards refuse masked-field probes (the equality oracle);
detectMaskedEchoWrites refuses masked round-trip writes with 400
VALIDATION_ERROR; getReadableFields keeps masked columns in the projection
so export headers match served rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
* test(rest): pin export screen-vs-CSV parity for partial masking; add changeset (objectstack-ai#8993)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
* regenerate artifacts on the merged tree (post-objectstack-ai#9113 merge) (objectstack-ai#8993)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
* gates: ADR-0122 alias form + Iso850 pin; translate the new maskingRule form strings (objectstack-ai#8993)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
---------
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…bjectstack-ai#9143)
* fix(pm): derive check:i18n from a metadata form module edit (objectstack-ai#9116)
A `.form.ts` change in packages/spec moves platform-objects' committed
metadata-form bundles, but no derived gate list could name check:i18n for
it: the gate walks packages/ for extract configs, packages/spec owns none,
and the gate's own path literals are its CLI/stale-dist prerequisites. The
family scored neither matched nor undetermined — printed nowhere. PR objectstack-ai#9113
paid one CI round trip plus a patch commit for that edge.
Two changes, one contract:
- scripts/i18n-bundle-surface.mjs — the config walk and the docstring-flag
parse move here and BOTH readers import them. dispatch-gates used to
carry a hand-written mirror of the gate's walk, described in its own
comment as mirroring it "exactly": a second contract that agrees until
one side moves, with nothing to report the day it stops.
- a second convention entry in CHANGE_KIND_GATES for a metadata form
module, with the population walked at runtime and the applicability read
from the configs' own documented flags — the day every config passes
--no-metadata-forms, no form module can move a committed bundle and the
entry stops firing by itself.
Both consumers declare the shared module as a bare module-body constant,
the shape check-type-check-coverage.mjs already uses: an import specifier
is not a discoverable watch hint, so without it a card editing the shared
enumeration would move two gates while deriving neither — the same blind
spot one layer down. Pinned live in the tool's self-test, both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011RB4waLuNbdruCo6X9oobm
* test(pm): fail the metadata-form cases by name, not on a TypeError (objectstack-ai#9116)
Reverse-verifying the new entry by making every extract config opt out
emptied the rendered lines, and the bare `formHit[0]` crashed the whole
self-test — one stack in place of 183 named verdicts, hiding every other
finding behind it. Same fallback discipline check-i18n-bundles.mjs states
for its own classifiers. Ablated again after the fix: 5 named failures,
all of them the new claims, nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011RB4waLuNbdruCo6X9oobm
* docs(pm): cite the filed follow-up for the uncovered type-registry edge (objectstack-ai#9116)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011RB4waLuNbdruCo6X9oobm
---------
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…upling (objectstack-ai#9219)
walkMetadataForms (packages/cli/src/utils/i18n-extract.ts) emits the
metadataForms i18n surface from two producers: METADATA_FORM_REGISTRY (the
17 *.form.ts modules, already covered by the form-module convention entry
in dispatch-gates.mjs) and DEFAULT_METADATA_TYPE_REGISTRY plus the
METADATA_FORM_REGISTRY module itself (packages/spec/src/kernel/
metadata-plugin.zod.ts, packages/spec/src/system/metadata-form-registry.ts).
Editing either of the latter two moves the same platform-objects bundles
PR objectstack-ai#9113 had to regenerate, and derived nothing from dispatch-gates.
Both are exactly two known files, not a runtime-enumerated population, so
this closes the gap the same way SURFACE_MODULE and
check-type-check-coverage.mjs's ROOT_PROGRAM_COUPLED_SCRIPT already do:
two bare module-body path-literal constants declared directly in
check-i18n-bundles.mjs (the script check:i18n resolves to), read by the
ordinary path-literal derivation with no new CHANGE_KIND_GATES entry.
Pinned live in dispatch-gates.mjs's own self-test (existsSync + a matched
classifyEntry verdict for each), so a renamed or moved registry module
reddens that self-test instead of the derivation silently going blind on
this edge again.
Fixesobjectstack-ai#9144
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude