Skip to content

page requires, #20312 stage ③ (engine half): at load, a page whose requires names an absent plugin is reported with the plugin named, and the draft→active promotion re-stamps requires #20870

Closed

Description

@objectstack-fleet

Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U), answering #20312's pm:retriage (5912514202) with the per-layer split (the #20618 pattern). ⛔ Not a claim, ⛔ not a dispatch. Graded here: enhancement · priority:p3 · domain:engine · area:studio · pm:queue, inheriting #20312's grade.

The ruling it carries

Ruling 5881821895 (letter A, staged ①②③) and 5902378057 (A + E on #20542) on #20312. Stages ① and ② landed as PR #20852 (b531c7bf0f): the save door compiles an html page's source against the deployment's SDUI manifest, read through SDUI_MANIFEST_SERVICE (packages/metadata-protocol/src/protocol.ts:4861; registered by os serve). This card is stage ③'s engine half.

Scope

  1. At load, report. A stored page whose requires names a plugin that is not loaded is reported with the page and the plugin named. It reads the same SDUI_MANIFEST_SERVICE channel the save door reads. ⛔ No second manifest reader.
    • The report is a boot/load diagnostic, and the page still loads: ruling A refuses at save, and reports at load.
    • If the load path the claim finds is in packages/runtime or packages/cli rather than the metadata layer, declare it as a cross-lane surface.
  2. Draft → active promotion re-stamps requires. Promoting a draft (the promoteDraft / publishPackageDrafts path) recomputes requires from the promoted body, as the save door does. ⛔ It never carries a stale stamp forward.
  3. Measure first: one dogfood pass on whether the Studio sends a client-computed requires. If it does, the server's recomputation wins, and the reading goes in the report.

Pins

  • A stored page naming an absent plugin: the load report names both, and the page is served.
  • A promoted draft's requires equals the save door's computation for the same body.
  • A page whose plugins are all present: no report (the control).

Then

#SPECHALF (domain:spec) flips the liveness row and makes the describe true. It is Blocked-by: this card.

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer: the spec half named above as #SPECHALF is #20871

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T13:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    #20871 (domain:spec) carries the liveness flip, the describe and docs sentences, the lint reason and the ADR-0087 entry, and is Blocked-by: #20870.

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01Ujdtvqs7ree7WyQmEDwEnG
    Account: os-litant (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20870-page-requires-load-and-promote
    Worktree: objectstack-issue-20870
    Domain: domain:engine
    Seat: domain:engine#2
    File surface: #20312 stage ③, engine half, under rulings 5881821895 (letter A, staged ①②③) and 5902378057 (A + E on #20542) on #20312, as the card body carries them. Stages ① and ② landed as PR #20852 (b531c7bf0).

    • At load, report. A stored page whose requires names a plugin that is not loaded is reported, naming the page and the plugin; the page still loads. The report reads the same SDUI_MANIFEST_SERVICE channel the save door reads (ObjectStackProtocolImplementation.resolveSduiManifest, packages/metadata-protocol/src/protocol.ts near :5053). ⛔ No second manifest reader. The load path is measured first. If it lives in packages/runtime or packages/cli rather than the metadata layer, that is a declared cross-lane surface (domain:cli), named in the report.
    • Draft → active promotion re-stamps requires. On the publishPackageDrafts path (protocol.ts), and any single-item promote the measurement finds, requires is recomputed from the promoted body with the save door's compiler (runtime-authoring-gate.ts). ⛔ A stale stamp is never carried forward.
    • Measure first: whether the Studio sends a client-computed requires, read from objectstack-ai/objectui at the console's pin (.objectui-sha). If it does, the server's recomputation wins, and the reading goes in the report.
    • Pins in packages/metadata-protocol beside protocol.runtime-authoring-gate.test.ts:
      • a stored page naming an absent plugin: the load report names both, and the page is served;
      • a promoted draft's requires equals the save door's computation for the same body;
      • all plugins present: no report (the control).
    • .changeset/20870-*.md.

    Stop on breach and explain in the report.

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20870,
      "status": "done",
      "branch": "claude/issue-20870-page-requires-load-and-promote",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21121",
      "session": "session_01Ujdtvqs7ree7WyQmEDwEnG",
      "premise_still_valid": true,
      "summary": "All three parts of stage ③'s engine half are delivered in packages/metadata-protocol, and no other lane is touched. (1) At load: loadMetaFromDb, the boot hydration of stored sys_metadata rows, prints one warn line [page_requires_plugin_absent] for each stored page whose requires names a namespace that no component in the deployment's SDUI manifest carries. The line names the page and every such namespace, and is printed only after the page has loaded; the page is still registered and served. The manifest comes from the same resolveSduiManifest() read on SDUI_MANIFEST_SERVICE that the save door makes, and the namespace set from one shared helper (manifestNamespaces) that the save door's requires judgement now uses too. With no manifest, nothing is judged, as at the save door (H2). (2) Promotion: promoteDraftForPublish serves both publishMetaItem and publishPackageDrafts. It now hands SysMetadataRepository.promoteDraft a new optional deriveActiveBody, which applies stampHtmlPageRequires to the draft row being promoted, against the manifest read at publish time. H3, measured on unmodified fed0db8f6: before this change, promotion carried a missing requires forward as missing and an agreeing list as spelled (4 pins red), and a disagreeing list was already refused by the gate. Nothing new is refused, so Clause-② stays no. (3) H4, the Studio, read at the console pin e420df310f5b: the Studio computes no requires on the client; the only client compile, page.tsx:606, drops it. It does send back the stored requires it last read: SourcePageEditor.tsx:149 patches only source, and ResourceEditPage.tsx:1443 and 1477-1481 send the whole draft. A server-side probe on this branch showed the cost: after a source edit that adds a plugin component, the stale stamp is carried into the draft and the publish answers 422 page-requires-disagrees-with-source. That refusal is stage ②'s accept set and is not changed here; it is raised below as a finding and an open question. Load hook chosen: the hydration itself, not kernel:ready. os serve registers the manifest before the first plugin init, and hydration runs in ObjectQLPlugin.start() after every init, so the manifest is readable when pages load. A host that registers after start() gets no load report for that boot; no in-repo host does. Declared deviation: sys-metadata-repository.ts is outside the claimed file surface, because the promotion's write lives in promoteDraft. The option is additive and named in the changeset (metadata-protocol minor). The browser dogfood pass was NOT MEASURED: H4 was answered by the code reading at the pin plus the server-side probe, without booting a console carrying the manifest. The card assignee was left alone; the PR assignee was set to os-litant.",
      "tests": "All at head c7b8b6399 (after two merges of origin/main) unless stated. (a) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 196 passed | 3 skipped (199), Tests 2938 passed | 19 skipped (2957). pnpm --filter @objectstack/metadata-protocol typecheck: exit 0, joined with && so the verify-lock VERDICT command-exit 0 covers both; tsc --listFiles includes protocol.runtime-authoring-gate.test.ts (1 hit). (b) New pins: 7 cases in the block 'stored html page requires at load and at draft promotion (#20312)' of protocol.runtime-authoring-gate.test.ts, all green. Run first against unmodified origin/main fed0db8f6: 4 failed | 3 passed (the 3 are controls) — 'expected [] to have a length of 1', 'expected undefined to deeply equal [ ui, plugin-kanban ]' twice (single and batch), and 'expected [ plugin-kanban, ui, ui ] to deeply equal [ ui, plugin-kanban ]'. (c) Ablations, one-shot via scripts/ablation-replace.mjs, no build leg (the suite imports src by relative path). A1: anchor 'if (hydrated && normalizedType === page) {' gets '&& false'; mutation landed (anchor 1 to 0, blob 3712159b to 4054739c); 1 failed | 6 passed, the reporting case; restored, blob == HEAD 3712159b and git diff HEAD empty. A2: 'const activeBody = opts.deriveActiveBody ? ... : draft.body;' replaced by 'const activeBody = draft.body;'; mutation landed (blob 8e75991e to fe63291b); 3 failed | 4 passed, the single, agreeing and batch promotion cases; restored, blob == HEAD. (d) Downstream consumers, direction ...@objectstack/metadata-protocol (packages that import it), targeted files at the first merge 7402cf72a: objectql --project local (sys-metadata-repository, protocol-publish-package-drafts, protocol-meta, plugin-restore-metadata-outage, protocol-boot-object-package-binding, registry-collision-order) 6 files / 174 tests passed; rest (meta-publish-package-scope, meta-published-overlay) 2 / 27; runtime (meta-published-runtime-publish) 1 / 5. Not re-run at c7b8b6399. (e) dispatch-gates --commands, no paths, merge base 5e470f8c1: 62 families; the derivation was identical at c7b8b6399. All 62 exit 0 at c7b8b6399, with each exit captured before any pipe. At 7402cf72a, check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, recorded as not measured); after a full turbo build of ./packages/* they answered real verdicts and passed, and again at c7b8b6399 ('105 published require entry point(s) across 66 package(s) load'; '2 published condition(s) measured from a real load'; '4 ledger entr(ies) re-measured ... none above its recorded number'). dispatch-gates --ran with per-line exit codes: '62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero)'. (f) check:driver-conformance: 50 covered / 0 DEBT / 0 exempt, before and after. (g) Lint, narrowed and declared. The population comes from eslint.config.mjs (packages/** TS, packages/spec ignored). eslint --no-inline-config --format json counted 4 files, 0 errors and 0 warnings. The config never enables type-aware linting (no parserOptions.project, no typed rules; noted at eslint.config.mjs:326-328), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's. (h) check:nul-bytes exit 0. NOT MEASURED: the integration tiers of downstream packages (CI's), and a browser dogfood pass of the Studio (see summary).",
      "mcp_calls": "0 — no MCP GitHub calls",
      "api_writes": "3 — each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft PR 21121; read-back says the 11338-byte body is stored identical); (2) label-write --assign os-litant → POST /repos/objectstack-ai/objectstack/issues/21121/assignees (read-back matches the target; the labels documentation, size/m, tests, tooling came from the path labelers, not from this write); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20870/comments. git push is not a REST write. No other write was made.",
      "open_questions": [
        {
          "question": "A Studio author who adds a plugin component to an already-published html page cannot publish the edit. The Studio sends back the requires it last read (SourcePageEditor.tsx:149 patches only source; ResourceEditPage.tsx:1443 and 1477-1481 send the whole draft, at objectui pin e420df310f5b). The stale stamp is kept on the draft, and the publish answers 422 page-requires-disagrees-with-source ('plugin-kanban' is used by the source but not listed), measured on this branch. Which end changes?",
          "options": [
            "A — objectui's page editor leaves requires out of the body it saves; the server stamps it fresh on every save, as the save door's hint already prescribes ('omit it'). One producer-side change; no contract change; no Clause-② change.",
            "B — the server treats requires as derived-only: it always re-stamps and never refuses a disagreeing list. This retires stage ②'s 'a hand-written requires that disagrees with the source is refused' (ruling 5881821895 ②), widens the save door's accept set (Clause-② yes, widening) and needs a new ruling.",
            "C — the server refuses a disagreeing requires only when it differs from the currently stored stamp. A heuristic and consumer-side tolerance, contrary to AGENTS.md Prime Directive 12. Not recommended."
          ],
          "recommendation": "A. Real need: measured — the Studio is the real producer of this body shape, and every edit that changes a published page's set of plugins reaches a 422 the author cannot explain, while REST/MCP authors who omit requires are unaffected. Long term: A keeps ADR-0080 §5 and ruling A intact (the source is the truth, requires is a derived stamp, a wrong hand-written list is refused) and fixes the producer, which is contract-first. AI safety: A keeps the loud refusal for an AI that hand-writes a wrong requires, where B would silently overwrite it, the tolerance that hides batch AI errors. Startup scope: A is one small objectui change with no new gate and no contract change; B reopens a ruling made two days ago."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: named real producer — the Studio page editor in objectui at pin e420df310f5b (SourcePageEditor.tsx:149 patches only source; ResourceEditPage.tsx:1033-1038 seeds the draft from the served document; ResourceEditPage.tsx:1443 and 1477-1481 PUT the whole draft with mode draft; no fromDraft for page). The server answer was measured on branch claude/issue-20870-page-requires-load-and-promote with that body shape: a draft save carries requires ['ui'] beside a source using plugin-kanban; then POST publish answers 422 INVALID_METADATA, page-requires-disagrees-with-source: 'plugin-kanban' is used by the source but not listed · fix lands in objectstack-ai/objectui (the page editor omits requires on save); file there with a back-link to #20312 and #20870 · dedupe words: page requires stale stamp, Studio round-trip requires, page-requires-disagrees-with-source, SourcePageEditor requires, ResourceEditPage requires",
        "carrier: 承接者:无 · the host's no-manifest boot line (packages/cli/src/utils/sdui-manifest.ts:270, 'Page source and `requires` not validated at save') stays true but now also covers the skipped load report; it could read 'at save or load' (pinned at packages/cli/src/utils/sdui-manifest.test.ts:551 and :566). Noted in PR 21121's Acceptance notes, not filed",
        "carrier: 承接者:无 · POST /packages/:id/publish (MetadataManager.publishPackage, packages/metadata/src/metadata-manager.ts:1811) is a loader-plane snapshot publish over the in-memory registry, not a sys_metadata draft promotion, and never ran the save door; observation only, noted in PR 21121's Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer — #20870's open question (report 5927166105): A, the Studio's page editor changes; answered in-seat, open to veto · filed objectstack-ai/objectui#11357

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T07:57Z. Open to the maintainer's veto.

    The question: a Studio author who adds a plugin component to an already-published html page cannot publish the edit. The editor sends back the requires stamp it last read, and stage ② refuses it as a hand-written list that disagrees with the source (422, page-requires-disagrees-with-source). Which end changes?

    Answer: A. objectui's page editor leaves requires out of the body it saves, and the server stamps it fresh on every save.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21121 @ c7b8b639 (#20312 stage ③, engine half: a stored page whose requires names an absent plugin is reported at load; promotion re-stamps requires) · Fixes #20870

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T08:29Z. Judged against GitHub, not the report (5927166105).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #21121 as 250dec897 · #20870 closed (completed)

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T09:14Z.


    Generated by Claude Code

  7. added 2 commits that reference this issue on Oct 7, 2026
    250dec8
    cfa4d74
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:engineenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions