Skip to content

finding: the ADR-0010 envelope-declaration gate cannot see UNREGISTERED_KIND_SCHEMAS — the blind spot that let #6362 survive #6245 #6931

Description

@os-zhuang

Split out of #6362 (PR #6900). Recording only — not claimed, not queued.

The observation

metadata-type-schemas.test.ts holds the ADR-0010 invariant that every metadata
type either declares ...MetadataProtectionFields or is named on an explicit
debt list. UNDECLARED_ENVELOPE is currently empty, and its own comment
calls that "the end state", so the gate reads as total coverage.

It is not. The gate iterates listMetadataTypeSchemaTypes(), which returns
Object.keys(BUILTIN_METADATA_TYPE_SCHEMAS) plus runtime extras — and
deliberately not UNREGISTERED_KIND_SCHEMAS. That exclusion is documented
and intentional (the trailing comment in metadata-type-schemas.ts explains
that enrolling webhook / connector / sharing_rule there would claim a KIND
status #6245 was careful not to grant, leaving #2657's B/C decision open).

The side effect was not intended, though: those three names are bound as real
parse doors — #6245 wired them to PUT /api/v1/meta/:type/:name — while the
invariant that exists to catch "declares no envelope" never runs over them.

Evidence that this is the actual cause, not a theory

connector sat in exactly that gap and the gate stayed green throughout:

So all three were judged one at a time, by hand, for a property the repo already
has an automated structural walker for.

Why this is observation-class, not a defect

Nothing a user hits today: as of PR #6900 all three bound kinds declare the
envelope, so the gap is currently empty of bugs. The exposure is prospective —
a fourth entry added to UNREGISTERED_KIND_SCHEMAS inherits the same
silence, and the next one may be non-strict too, in which case the failure is
again a silent strip rather than a 422.

Possible directions (deliberately not chosen here)

  1. Give the existing suite a second it.each over Object.keys(UNREGISTERED_KIND_SCHEMAS)
    asserting only the envelope-declaration property — without enrolling them
    in listMetadataTypeSchemaTypes(), so none of the other KIND obligations
    (create seeds, MetadataTypeSchema membership, the campaign count) attach and
    Studio metadata coverage gaps: surface remaining types + promote un-typed concepts #2657 stays unprejudged.
  2. Export the bound-kind names so the walk derives the set rather than
    hand-listing it.
  3. Leave as-is and rely on review, if the set is expected to stay at three.

Direction 1 looks like the cheap one and appears to preserve every boundary
#6245 drew, but it is a decision about a contract surface's coverage rules, so
it should be triaged rather than assumed.

Refs: #6362 / PR #6900 (connector, the silent-strip case), #6245 (bound the three
doors; fixed sharing_rule's 422 case), #4001 batch 11 (webhook's spread),
#2657 (the open B/C kind-promotion decision), ADR-0010.

Activity

  1. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): promoted finding → pm:queue (domain:spec kept). Three real parse doors (webhook/connector/sharing_rule, wired to PUT /meta by #6245) sit outside the ADR-0010 envelope invariant while the gate's empty debt list reads as total coverage — a gate that overstates its own reach. Scope fence: the fix enrolls them in the test's iteration only (assert envelope posture or an explicit debt entry); it must NOT grant them KIND status — #2657's B/C decision stays open, exactly as the schemas file's comment intends.


    Generated by Claude Code

  2. self-assigned this
    on Aug 9, 2026
  3. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    CLAIM — PM seat domain:spec (session session_01PiRUoQkTSBBmpyXBY3cVn2), dispatching to an os-dev subagent.

    • Branch: claude/issue-6931-envelope-gate-unregistered-kinds
    • Worktree: ../objectstack-issue-6931 (per-task, PD Migrate documentation site to Fumadocs with monorepo structure and shared content #11)
    • Dispatch basis (triage promotion, quoted as ruling): "Scope fence: the fix enrolls them in the test's iteration only (assert envelope posture or an explicit debt entry); it must NOT grant them KIND status — Studio metadata coverage gaps: surface remaining types + promote un-typed concepts #2657's B/C decision stays open, exactly as the schemas file's comment intends." = the body's direction 1, with direction 2 (exporting the bound-kind names so the walk derives the set) permitted only if it stays within the fence.
    • File surface: packages/spec/src/**/metadata-type-schemas.test.ts (+ possibly a names export in metadata-type-schemas.ts), nothing that changes runtime behavior.

    Generated by Claude Code

  4. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    PM bookkeeping: pm:dispatched → pm:queue. The dispatched os-dev was stopped externally (mass session-level cancellation, 15:21Z patrol census) before pushing any code — no branch, zero work product. Claim released; card returns to the queue intact. The fenced brief (test-iteration enrollment only, no KIND status) remains valid; re-dispatch awaits the maintainer's word.


    Generated by Claude Code

  5. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    RE-CLAIM — PM seat (session session_01PiRUoQkTSBBmpyXBY3cVn2), re-dispatched as a mode:cloud card under the maintainer's re-acceleration (2026-08-09 chat). Branch claude/issue-6931-envelope-gate-unregistered-kinds. The scope fence in the triage promotion and the 13:37Z claim comment remain binding (test-iteration enrollment only; no KIND status; #2657 stays open).


    Generated by Claude Code

  6. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    OS-DEV-REPORT

    {
      "issue": 6931,
      "status": "complete",
      "session": "session_01JJpiZS4AgkrDYwZ2Amh5mw",
      "branch": "claude/issue-6931-envelope-gate-unregistered-kinds",
      "worktree": "../objectstack-issue-6931",
      "commit": "775d648a3462f6860c57315d26baa4f0581fb32f",
      "pr": 7116,
      "pr_state": "draft",
      "landing": "left to the PM — no auto-merge, no queue, not armed",
      "premise": {
        "verified_on": "origin/main f5a9bc2",
        "a_invariant_and_empty_debt_list": "HOLDS — UNDECLARED_ENVELOPE is `new Set<string>([])`, commented as the end state",
        "b_iteration_excludes_unregistered_kinds": "HOLDS — listMetadataTypeSchemaTypes() returns 26 names, none of webhook/connector/sharing_rule",
        "c_all_three_declare_envelope_today": "HOLDS — each resolves 1 object shape and declares _packageId; new assertion GREEN on arrival, value is prospective",
        "verdict": "alive — proceeded"
      },
      "implementation": {
        "test_file": "packages/spec/src/kernel/metadata-type-schemas.test.ts",
        "second_it_each_over": "listUnregisteredKindSchemaTypes()",
        "asserts": "envelope posture only — declares ...MetadataProtectionFields, or sits on UNDECLARED_ENVELOPE_UNREGISTERED (own constant, empty, reverse-pinned)",
        "names_export": "listUnregisteredKindSchemaTypes() in packages/spec/src/kernel/metadata-type-schemas.ts — direction 2, names only, grants nothing",
        "walker_reuse": "MEASURED — both iterations are in one file and call the same objectShapes walker and the same rejectedEnvelopeKeys probe; no second copy exists to drift",
        "deliberately_omitted": "no unknown-key/posture case — sharing_rule is strict, connector is not, and that choice is #2657's"
      },
      "fence": {
        "ruling_followed": "test-iteration enrollment only; no KIND status; #2657 stays open",
        "list_output_byte_identical": true,
        "list_output": "[\"action\",\"agent\",\"api\",\"app\",\"book\",\"capability\",\"dashboard\",\"dataset\",\"datasource\",\"doc\",\"email_template\",\"field\",\"flow\",\"hook\",\"job\",\"mapping\",\"object\",\"page\",\"permission\",\"position\",\"report\",\"seed\",\"skill\",\"tool\",\"translation\",\"view\"]",
        "kind_obligations_checked": [
          "#4001 campaign count — still 25 closed / 26 total, green (also green in reverse run 2 with a fake fourth entry present)",
          "metadata-create-seeds.test.ts — green",
          "capability-metadata-kind.test.ts, metadata-type-api-registration.test.ts — green",
          "MetadataTypeSchema / DEFAULT_METADATA_TYPE_REGISTRY — metadata-plugin.zod.ts not in the diff",
          "check:stack-collection-maps, check:meta-type-normalized — pass"
        ],
        "new_pin": "'stays OUT of the registered-kind set' — fails if any of the three enters listMetadataTypeSchemaTypes(); a signpost for a future #2657 promotion, not a veto"
      },
      "reverse_verification": {
        "direction_predicted_first": true,
        "run_1": {
          "edit": "removed ...MetadataProtectionFields from ConnectorSchema (connector.zod.ts:880), then restored",
          "predicted": "new 'connector DECLARES' RED naming connector; new 'connector does not REJECT' GREEN (non-strict ⇒ silent strip); BOTH original suites fully GREEN",
          "measured": "1 failed | 120 passed (121) — the single failure is 'connector DECLARES the protection envelope'; all 107 original cases green",
          "match": true
        },
        "run_2": {
          "edit": "added fake_kind: z.object({ name: z.string() }) to UNREGISTERED_KIND_SCHEMAS, then removed",
          "predicted": "'fake_kind DECLARES' RED; its other three new cases GREEN; original suites GREEN incl. the 25/26 count; 125 total, 1 failed",
          "measured": "1 failed | 124 passed (125) — 'fake_kind DECLARES the protection envelope' RED; campaign count green",
          "match": true
        },
        "both_edits_reverted": true
      },
      "gates": {
        "lint_yml_run_one_by_one": "all green",
        "eslint": "pass",
        "spec_tsc_noEmit": "pass",
        "turbo_typecheck": "121/121 pass",
        "examples_typecheck": "pass",
        "downstream_contract_typecheck": "pass",
        "root_check_gates": "45 run; app-nav-i18n / i18n / i18n-coverage first reported 'nothing was measured — build first', all pass after turbo build (70/70)",
        "check_type_check_coverage_and_debt": "pass — 33 ledger entries re-measured, none above its recorded number",
        "check_export_origins": "NEW TODAY — the brief's prediction did NOT hold. The change is not purely test-only: the fenced fix needs the names export, so the one added export made api-surface/kernel.json AND export-origins/kernel.json stale (0 breaking, 1 added). Regenerated exactly those two (gen:api-surface, gen:export-origins) — one line added to each — then check:generated reports all 11 artifacts up to date.",
        "adr_0087": "node scripts/check-adr-0087-registration.mjs --base origin/main → 'no declared-breaking changeset (1 non-breaking changeset(s) seen)' — nothing owed",
        "changeset": "INCLUDED (.changeset/envelope-gate-unregistered-kinds.md, @objectstack/spec patch). Analog read: #7090 (test(spec) adding a checked-in artifact + gate) carried the same. The skip-changeset 'releases nothing' route does not apply here — this PR adds a public export to a published package.",
        "tests": "full suite green, 134/134 turbo tasks, zero failures. @objectstack/lint run separately: 68 files / 1770 tests all pass — two earlier full-suite passes hit 5s and 30s vitest timeouts in its corpus sweeps under parallel container load, on cases unrelated to this diff."
      },
      "findings_filed": [],
      "notes": "Only deviation from the brief worth naming: the brief expected check:export-origins to be untouched by a test-only change. The fenced fix is not purely test-only — a hand-listed set of three names would not inherit a fourth entry, which is the whole prospective value the card is about — so the export was taken (direction 2, explicitly permitted) and the two export-surface artifacts regenerated. No other scope was added."
    }

    Generated by Claude Code

  7. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    Remote CI on PR #7116: fully green — 26 check runs, 24 success, 2 skipped by paths filter (Console Pin Gate, Build Docs). Includes TypeScript Type Check (the required job carrying the generated-artifact gates, so the regenerated api-surface/kernel.json + export-origins/kernel.json are confirmed current server-side), ESLint, Check Changeset, Test Core 1–3/3, Dogfood Regression Gate 1–3/3, Temporal Conformance (live PG + MySQL), and Spec property liveness.

    PR is draft, auto-merge not armed, not in the merge queue — landing is the PM's, as briefed.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions