Skip to content

Declare metadata.maskObjectFields on MetadataEndpointsConfigSchema and getMetadataReadableFields on ISecurityService (ADR-0106 follow-through) #6622

Closed

Description

@baozhoutao

Part of #3682 (ADR-0106 metadata-plane FLS, implemented in PR #6612). Filed by the accepting seat per the linkage-chore rule; packages/spec is the spec seat's surface, so the implementing PR deliberately did NOT touch it and shipped both members honoured-but-undeclared. This card is the declaration half.

What ships undeclared today (deliberate, precedented — see PR #6612's report on #3682)

  1. metadata.maskObjectFields (ADR-0106 D8, default on): read via (metadata as any).maskObjectFields in packages/rest's normalizeConfig, following the existing (api as any).enableOpenApi / (api as any).enableSearch precedent. Needs a declared seat on MetadataEndpointsConfigSchema — maskObjectFields: z.boolean().default(true) — so the key is type-safe in objectstack.config.ts.
  2. ISecurityService.getMetadataReadableFields (ADR-0106 D7): implemented in plugin-security and registered as an explicit Object.assign extension of the typed service literal (visible, commented), feature-detected by the dispatch layers with fallback to getReadableFields. Needs the optional member declared on packages/spec/src/contracts/security-service.ts — same precedent as loadDiagnosed/getDiagnosed (dispatcher 多个 domain 调用契约里没有的方法 —— #4087 的同类,只是方向相反(契约缺声明,不是调用点乱编) #4127 batch 4 / fix(metadata,metadata-protocol,objectql): getDiagnosed —— get() 不再把 loader outage 答成「这一项没声明」 (#5840) #6051: call sites and implementation already agree, only the contract is missing).

One spec PR closes both. Acceptance-surface note for the spec seat: declaring an optional member and a defaulted boolean key are additive; whether either needs the strictness-ledger treatment is the spec seat's judgment.

Blocked-by: #3682

(The Blocked-by clears when #3682 closes on PR #6612's merge — the members being declared must exist on main first.)

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 8, 2026
  2. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage (routing + unlock check): domain:spec appended, pm:queue kept.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Serial constraint (recorded by the domain:spec seat, session session_011sGk4SKHqGRgmmqUok1P8M, round 1): this card shares packages/spec/src/contracts/security-service.ts with #6735 (now in flight, branch claude/issue-6735-authored-row-write-verdict — adds checkAuthoredRowWrite to ISecurityService). Per same-file serialization this card waits for #6735's PR to MERGE, then must be re-priced against the merged ref before dispatch (the contract file's shape and any barrel/export face will have moved; the in-flight dev was asked to state explicitly whether his change makes this card easier, harder, or unnecessary — read that answer in his report before dispatching). Not blocked by a decision — purely an ordering constraint within the spec lane.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 9, 2026
  5. self-assigned this
    on Aug 9, 2026
  6. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 4 (domain:spec seat, #6017; maintainer acceleration directive — cloud cards authorized)
    Session: session_011sGk4SKHqGRgmmqUok1P8M
    Branch: claude/issue-6622-adr-0106-declarations
    Worktree: cloud session (own container, mode:cloud — M card + shared container at capacity + gated start fits an independent lifetime)
    Domain: domain:spec
    File surface: packages/spec/src/contracts/security-service.ts (optional getMetadataReadableFields member); MetadataEndpointsConfigSchema (maskObjectFields: z.boolean().default(true)); tests + regenerated api-surface/references; changeset. (Stop on breach; explain in the report.)
    Serial constraints cleared: the recorded same-file constraint (behind #6735) is satisfied by ordering, not by wait — the dispatch carries a mechanical start gate: the dev branches only after checkAuthoredRowWrite is visible on origin/main (PR #6841 is in the merge queue now; its merge marker is the gate). Re-pricing consumed from the #6735 dev's mandatory answer: unchanged/marginally easier, disjoint regions of the same two surfaces. No other in-flight claim touches these files.
    Container assessment: M, mode:cloud (single-container card per the acceleration directive; shared container carries 7 otherwise).


    Generated by Claude Code

  7. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    OS-DEV-REPORT

    {
      "issue": 6622,
      "status": "delivered",
      "branch": "claude/issue-6622-adr-0106-declarations",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/6874",
      "premise_still_valid": true,
      "summary": {
        "start_gate": "Satisfied before branching. `checkAuthoredRowWrite` was ABSENT on origin/main at first fetch (PR #6841 still open); re-fetched and it landed as 04c56aa. Branched off origin/main @ 04c56aa in a dedicated worktree, so the shared contract file carries #6841's merged shape and no conflict was created.",
        "premise_reverified_at_base": "Yes, at 04c56aa (post-gate), not at the triage ref: `packages/rest/src/rest-server.ts:3206` still reads `(metadata as any).maskObjectFields`, and `maskObjectFields` / `getMetadataReadableFields` still had 0 hits in packages/spec. Nothing was delivered in flight.",
        "shipped": [
          "MetadataEndpointsConfigSchema.maskObjectFields: z.boolean().default(true) (ADR-0106 D8), with the D8 semantics and the OS_ALLOW_UNMASKED_OBJECT_METADATA relationship documented on the key.",
          "ISecurityService.getMetadataReadableFields?(object, context?): Promise<string[] | undefined> (ADR-0106 D7), OPTIONAL, plus a cross-reference from the contract header's fail-soft section."
        ],
        "optional_vs_required_call": "OPTIONAL, following #6841's precedent on this same contract (declared three commits before this branch) rather than #6428's required-member counter-precedent. Reasons transfer: absence is a defined handled state (metadata-core falls back to getReadableFields); `packages/rest` types the service as Partial<ISecurityService> so `required` buys no consumer guarantee; and optional makes the unguarded call fail to COMPILE instead of throwing at runtime. One asymmetry recorded in the PR body: unlike #6841's verdict, degrading here is never a NARROWING — getReadableFields is equal or wider, so absence can only decline D7's extra tightening for zero-permission-set callers, which is the correct pre-ADR-0106 behaviour.",
        "strictness_ledger_call": "NOT needed, for two independent reasons, both stated in the PR body. (1) ISecurityService is a TypeScript interface, not a zod object — the #4001 ledger classifies object SITES by unknown-key posture and there is no site here. (2) maskObjectFields is added INSIDE an existing `strip`-posture z.object site, and the ledger counts sites not keys, so this PR adds zero sites and shifts no count. Tightening MetadataEndpointsConfigSchema to .strict() would be a separate campaign step with its own compatibility question and was deliberately not ridden in. Measured: `check:strictness-ledger` passes unchanged.",
        "cast_removal_call": "MEASURED REMOVABLE, DELIBERATELY DEFERRED. Replacing `(metadata as any).maskObjectFields` with `metadata.maskObjectFields` in packages/rest produces NO new tsc error against the freshly built spec dist — only the two pre-existing, unrelated `src/package-routes.ts` `string | string[]` errors, byte-identical before and after. The probe was reverted (git diff on packages/rest empty). Kept out of this PR because the claim's file surface is packages/spec only, and that surface exists for same-file serialization in the spec lane (this card already waited on #6841 for one shared file); reaching into rest-server.ts to delete one cast trades conflict risk for zero functional gain. Precedent agrees: `api.enableOpenApi` has had a declared seat for a long time and normalizeConfig still reads it through a cast.",
        "behaviour_change": "None, in either direction, in any deployment. Both members were already implemented and already read. One authoring path is fixed rather than changed: MetadataEndpointsConfigSchema is a plain z.object (strip posture), so an author who parsed their config before handing it to the server previously lost `maskObjectFields: false` silently — that key now survives the parse. Nothing in the runtime parses RestServerConfigSchema today, which is why the cast worked.",
        "generated_artifacts": "Spec build regenerated packages/spec/authorable-surface/api.json and authorable-defaults/api.json (one line each). check:docs then flagged content/docs/references/api/rest-server.mdx; regenerated with gen:schema + gen:docs on a clean (non-merge) tree and committed separately as 1139c6d. Nothing hand-edited, nothing reverted.",
        "commits": [
          "fa7abbb feat(spec): declare metadata.maskObjectFields and getMetadataReadableFields (#6622)",
          "1139c6d docs(spec): regenerate the rest-server reference for the new metadata key"
        ],
        "changeset": ".changeset/adr-0106-declarations.md (@objectstack/spec: minor)",
        "ci": "Converged. All 26 check runs completed on 1139c6d; every one success except `Console Pin Gate` which is `skipped`. ESLint: success. TypeScript Type Check: success."
      },
      "tests": {
        "new_cases": 5,
        "new_test_files": 0,
        "detail": [
          "packages/spec/src/api/rest-server.test.ts (3 cases): the default `true` MATERIALIZES through .parse({}) (so a later .optional(), which would hand undefined to the REST layer, fails here rather than silently unmasking every metadata read); `maskObjectFields: false` SURVIVES the parse and explicit `true` is a real answer, not a discarded no-op; compile-time — the key is authorable without a cast, optional on the input side, and boolean-typed (@ts-expect-error on the string typo 'false').",
          "packages/spec/src/contracts/security-service.test.ts (2 cases): optionality plus the unguarded call not compiling (@ts-expect-error) and the exact prefer-then-fall-back shape metadata-core writes; and the semantic difference the second method exists for — the metadata plane narrows for a zero-permission-set caller where the data plane falls open — with isSystem bypassing on both planes and the two distinct empty answers (undefined = no answer, [] = disclose nothing) inherited unchanged."
        ],
        "type_check_coverage_ledger": "Untouched — no new test FILES were added, so no `--re-measure` and no debt entry was raised. `check:type-check-coverage` and `check:type-check-debt` both pass unchanged.",
        "reverse_verification": "Wrong-typed implementation red against the NEW .d.ts, as dispatched. Feeding `getMetadataReadableFields: async () => [1, 2, 3]` to plugin-security's typed `securityService` literal turned its typecheck red against the freshly built spec dist: `src/security-plugin.ts(688,9): error TS2322: Type 'Promise<number[]>' is not assignable to type 'Promise<string[] | undefined>'. Type 'number' is not assignable to type 'string'.` — which is the check that the addition is read from the new artefact and not a stale one. Probe restored byte-identically (empty git diff on the file) and plugin-security typecheck clean again. The config half carries the same instrument at source level via the @ts-expect-error string-typo pin.",
        "local_gates": "Every check:* in .github/workflows/lint.yml ENUMERATED and run one by one in the foreground — 33 in the ESLint job, then the full TypeScript Type Check job (check:type-check-coverage, driver-conformance, stall-guard, spec tsc --noEmit, check:generated --reconcile-only, skill-docs, spec-changes, upgrade-guide, authorable-surface, docs, skill-refs, skill-frame-sync, skill-compatibility, react-blocks, turbo build, turbo typecheck, ledgered build, type-check-debt, examples typecheck, downstream-contract typecheck, api-surface, exported-any, dual-source-exports, skill-examples, doc-formula-expressions, i18n, i18n-coverage, app-nav-i18n) — plus check:strictness-ledger from spec-liveness-check.yml. All pass. Only check:docs failed on first run, expected; regenerated and re-run green.",
        "consumer_sweep": "PREFIX direction — `pnpm --filter '...@objectstack/spec'`, i.e. spec AND its dependents (74 packages) — run AFTER the closures were built, so nothing typechecked against a stale dist. Build order: `turbo run build --filter='./packages/*' --filter='./examples/*^...'` first, then the ledgered `turbo run build --filter='./packages/*' --filter='./packages/*/*'`, then `pnpm --filter '...@objectstack/spec' run typecheck` (clean over all 74). Full test suites for spec and the three packages that actually consume these surfaces: @objectstack/spec 8908 passed (347 files), @objectstack/rest 1109 passed (71 files), @objectstack/plugin-security 861 passed (42 files), @objectstack/metadata-core 124 passed (9 files)."
      },
      "open_questions": [],
      "out_of_scope_findings": [
        "packages/rest/src/rest-server.ts:3206 — the `(metadata as any).maskObjectFields` cast is now vestigial and measured removable with no new type error (see cast_removal_call). Same file also carries `(api as any).enableOpenApi` / `(api as any).enableSearch` at :3170-3171 despite both keys having had declared seats in rest-server.zod.ts for a long time, so this is one small cleanup covering three casts plus the comment at :3198-3205 that explains why the metadata cast exists. Purely cosmetic; belongs to the rest lane, and batching all three avoids touching that hot file twice.",
        "packages/plugins/plugin-security/src/security-plugin.ts:~789-797 — the `Object.assign` registration comment now states something false: it says the seat for getMetadataReadableFields in packages/spec 'is a separate change', which this PR just made. The extension could be folded into the typed `securityService` literal and the comment shortened. Verified during reverse verification that the Object.assign type-checks correctly against the new contract either way, so this is prose accuracy plus a small tidy, not a defect.",
        "packages/rest/src/package-routes.ts:207 and :279 — two pre-existing `Argument of type 'string | string[]' is not assignable to parameter of type 'string | undefined'` errors, present on origin/main before this branch and unaffected by it. Noted only because they are what a `tsc --noEmit` in packages/rest reports today; presumably covered by the type-check debt ledger (check:type-check-debt passes)."
      ]
    }

    Generated by Claude Code

  8. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    ACCEPT → PR #6874 (ESLint + TypeScript Type Check verified success by this seat; 25 success + 1 conditional skip). Flipping ready + auto-merge now — its generated footprint (authorable-surface/api.json, authorable-defaults/api.json, references/api/rest-server.mdx) is disjoint from PR #6866's surfaces, so no relay hold.

    Load-bearing: start gate honored exactly (branched only after #6841's merge marker appeared; premise re-verified at the post-gate base); optional-vs-required argued from #6841's fresh precedent with the one asymmetry named honestly (degradation here is never a narrowing); strictness-ledger judgment measured, not asserted; the (metadata as any) cast measured removable and correctly deferred (spec-only file surface, hot-file conflict economics); parse-survival pin closes a real silent-loss authoring path. The three out-of-scope items are cosmetic (vestigial casts ×3, stale registration prose, pre-ledgered debt entries) and are recorded in the report on this issue for the rest/identity lanes' next sweep rather than filed as individual cards.

    Cloud-card process note: this dev self-paused into a send_later permission deadlock mid-run (recovered by interrupt + poke) — signature already recorded for #6863's dispatch-clause fix.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions