Skip to content

[P2] 运行时授权门扩到 object 写入 + 全量 runtime-safe 规则快照(#4463 P1 之后) #4716

Closed
2 of 2 issues completed

Description

@os-zhuang

Blocked-by: objectstack-ai/objectstack#4463(PR #4715 合并前不要派发)

#4463 P1 把作者时规则接上了运行时写入闸门(saveMetaItem 的 active 保存 + promoteDraftForPublish),但只挡 flow 类型,且只跑不需要全栈快照的规则族。这是 PM 在派发时刻意收窄的,不是遗漏 —— 本单是它记账下来的下一步。

为什么 P1 停在 flow(裁定理由,接手前先读)

object 写入是产品里最热的元数据路径(Studio 每次改字段都走它),一个误判 422 的爆炸半径是租户唯一的编辑面 —— 严格劣于暂时留一个覆盖缺口。#4463 的全部验收标准也都是 flow。所以 P1 的选择是:先上 flow,等闸门跑过真实流量、有了误报预算再放宽。

本单要付的代价就是那份误报预算。 不要把它当成「把 runtimeTypes 多写一个值」的一行改动交付。

范围

明确不在本单

  • advisory findings 进响应 + objectui 渲染 —— 见另一单
  • validateSecurityPosture 合进统一表(它在 object 上已经被 plugin-security 的 registerAuthoringGate 挡着,是合并不是缺口)

关联

#4463(P1 本体)、PR #4715、#4409 / #4445(CLI 三命令 registry)、ADR-0033、ADR-0087

Activity

  1. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    迁移:domain:engine-core → domain:metadata(维护者 2026-08-07 批准拆分;座位贴 #6367,SKILL PR #6370)。FLAG 裁定随迁:运行时授权门 P2 的门线落 saveMetaItem / promoteDraftForPublish(metadata-protocol 写路径);规则集与 surface 注解的 lint 半边(spec-tooling)在派发时按 rule 2 contract-first 拆出,不整卡跨席。分诊会话:session_01BickTBKm2JYSNnrtPT8ysa。


    Generated by Claude Code

  2. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Successor-pricing note from PR #6708 (#6285), recorded before this card is dispatched — domain:metadata PM, session session_01W6bLax4KMrSfnE1ydFU8Dw:

    (Still queued behind the engine-core seat's #5839 claim; unchanged.)


    Generated by Claude Code

  3. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    Measurements carried over from #6715, which closed as mis-scoped into this card (domain:metadata seat, session session_01W6bLax4KMrSfnE1ydFU8Dw). Recording, not claiming — this is pricing input for whoever takes this card up.

    #6715 asked to widen one rule (validateEmptyCombinators) from runtimeTypes: ['flow'] to the other filter-carrying types. It was dispatched with a stop condition, the condition fired, and no code was written. Four results are worth having on this thread:

    1. The gate dispatches on flow alone, across the whole rule family. Measured on origin/main: runtimeGatedTypes() returns exactly ['flow'], and all six rules on the runtime-publish surface declare runtimeTypes: ['flow'] (validateStackExpressions, validateEmptyCombinators, validateReferenceIntegrity, validateFlowTriggerReadiness, validateApprovalApprovers, lintFlowPatterns — 6 of 37 total rules reach that door at all). Every other type measures gated-by=(NONE), with errors=0 rulesRun=[]. The only out-of-band gate at that door is ADR-0094's registerAuthoringGate, registered exactly once, on object, by plugin-security — and it judges OWD security posture, not authored filters. So this card is not "one more type for one rule"; it is the whole frontier, which is what its own text already says.

    2. The type list in circulation is wrong — seven surfaces, not four. EMPTY_COMBINATOR_SURFACES walks objects, views, pages, dashboards, reports, datasets, apps; all seven are real MetadataTypes with allowRuntimeCreate: true. The "four types" figure that appears in #6715's body and in the registry comment at packages/lint/src/authoring-rules.ts:502-510 undercounts by three.

    3. There is a mechanical blocker behind three of those seven. report / dataset / app have no TYPE_TO_STACK_KEY entry (packages/lint/src/runtime-gate.ts:68-78), and authoring-rule-wiring.test.ts:387 fails on any gated type lacking one. So "one line" holds only for the four mapped types; opening the frontier properly means deciding what those three map to, or deciding deliberately that they stay out.

    4. The false-positive-budget precondition cannot be paid from this repo's corpus as it stands. #6715's corpus measurement returned 0 would-be-refused across 94 non-flow items in showcase/CRM/todo — but that 0 is weak by construction: these rules are already gating on all types for os validate / os build / os lint, so anything in a config-file corpus is already refused there. The corpus that would actually exercise the question is stored sys_metadata overlay rows, which do not exist in-repo. Whoever takes this card up should plan for where that replay corpus comes from, rather than discovering mid-flight that the in-repo corpus cannot answer it.

    One more note that changes this card's stakes rather than its scope: #6710 landed today (PR #6971, 30283263c). The runtime publish gate is now active by default on every kernel, keyed on a declared authoring channel instead of the environmentId proxy — so more topologies reach this door than did yesterday, including the CLI host-config assembly. The frontier this card moves is now wider than when it was filed.

    The registry comment at authoring-rules.ts:502-510 was deliberately left untouched by #6715: its stated condition ("once #4463 P2 opens them at the gate") is measurably still unmet — #6710 settled which kernels activate the gate, not which types it dispatches on. It should be flipped by whichever PR actually opens the types, i.e. this one.

    Lane note: the landing site is packages/lint/src/authoring-rules.ts — the devx lane's package; metadata-protocol consumes the table via @objectstack/lint/runtime. This seat is not claiming the card.


    Generated by Claude Code

  4. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    PM status correction + dispatch ordering (domain:metadata seat, session session_01W6bLax4KMrSfnE1ydFU8Dw). Not dispatched this round, and the reason has changed.

    Correcting two things this seat recorded wrongly.

    1. The "[metadata-core] sys_view_definition 的"活跃行唯一"从未生效——归档视图仍占用 (name, organization_id, owner) 名额 #5839 blocker" was never real during this term. This card (and [P2/P3] 运行时授权门的 advisory findings 要进响应并被 Studio 渲染 + docs「the gate」补第四列(#4463 收尾) #4717) carried "still queued behind [metadata-core] sys_view_definition 的"活跃行唯一"从未生效——归档视图仍占用 (name, organization_id, owner) 名额 #5839", including in a comment this seat wrote on 08-08. [metadata-core] sys_view_definition 的"活跃行唯一"从未生效——归档视图仍占用 (name, organization_id, owner) 名额 #5839 closed as completed on 2026-08-07T17:56:45Z — before this seat was seated. It was inherited as a sentence and repeated without checking whether its precondition still held.
    2. This seat's own lane note was wrong. On 08-09 (comment 5230446731) I wrote that the landing site is packages/lint and that this seat was not claiming the card. That conflated two different things: [finding] validateEmptyCombinators gates the runtime publish path for flow only — the other filter-carrying metadata types reach the same door ungated #6715 measured where one rule's registration lives (packages/lint, devx), but this card's subject is the gate itself extending to object writes, and the maintainer's 2026-08-07 migration ruling put that line squarely on saveMetaItem / promoteDraftForPublish — metadata-protocol, this seat's surface. The lint half is what gets split out under rule 2, not the card.

    Why it is still not dispatched this round — a real region conflict, not a blocker. #4717 was dispatched minutes ago and is changing saveMetaItem's response envelope. This card's gate line lands in saveMetaItem / promoteDraftForPublish. Same function. This lane's parallel-authorship authorisation is explicitly "region-disjoint only; same function or same region never in the same batch", so this card follows #4717 rather than riding beside it.

    Two pricing inputs that must not be lost when it is taken up (measured by #6715, carried here in comment 5230446731):

    • The gate dispatches on flow alone across all six rules on the runtime-publish surface — this card is the whole frontier, which its own text already says, and it forbids delivery as a one-line runtimeTypes edit.
    • The false-positive-budget precondition cannot be paid from this repo's corpus as it stands. The corpus that would exercise the question is stored sys_metadata overlay rows, which do not exist in-repo. Whoever takes this card up should settle where that replay corpus comes from before dispatch, rather than discovering it mid-flight. That is the genuine open item on this card now — not [metadata-core] sys_view_definition 的"活跃行唯一"从未生效——归档视图仍占用 (name, organization_id, owner) 名额 #5839, and not lane ownership.

    Also relevant since filing: #6710 landed (30283263c), so more topologies reach this gate than when the card was written, and the registry comment at authoring-rules.ts:502-510 ("once #4463 P2 opens them at the gate") is still measurably unmet — it should be flipped by whichever PR actually opens the types, i.e. this one.


    Generated by Claude Code

  5. hotlong commented on Aug 15, 2026

    @hotlong
    Contributor

    domain:metadata seat — not dispatching, and escalating the one precondition that has blocked this card for six days without being named as a decision. PM session session_01XeQRiAa7vYRVX5Fog7Zby8. ⛔ No label changed, ⛔ not claiming.

    What I verified before writing this

    The Blocked-by: line in the body is discharged. #4463 closed completed 2026-08-02T22:02:24Z via merged PR #4715. I checked rather than reading the line's presence as state — that exact substitution cost me two wrong reports on other cards earlier today, and this body's "PR #4715 合并前不要派发" is precisely the shape that invites it.

    The 2026-08-10 region conflict recorded in comment 5236052336 (#4717 rewriting saveMetaItem's response envelope) is five days stale and is not why this card is sitting.

    The real blocker is a decision nobody has been asked to make

    From the previous seat's own measurement, carried in 5230446731 and re-stated in 5236052336:

    The false-positive-budget precondition cannot be paid from this repo's corpus as it stands. The corpus that would actually exercise the question is stored sys_metadata overlay rows, which do not exist in-repo.

    And why the in-repo corpus cannot substitute, which is the sharp part: #6715 measured 0 would-be-refused across 94 non-flow items in showcase/CRM/todo — but that zero is weak by construction. These rules are already gating on all types for os validate / os build / os lint, so anything living in a config-file corpus has already been refused there. A config-file corpus cannot produce a violating row; only a tenant's stored overlay can.

    This card's own body makes that budget an acceptance criterion, not a nicety:

    上 object 之前需要一批真实租户元数据的回放,证明不会把既有合法 object 判红

    and states the stake plainly: object writes are the hottest metadata path in the product (every Studio field edit), so a mistaken 422's blast radius is the tenant's only editing surface.

    ⇒ There is no dispatch that can satisfy this card's own acceptance criterion today. Sending a dev now buys either a run that stops at the corpus, or — worse — one that pays the budget with the in-repo corpus and reports a green 0 that is green by construction.

    The question for you (this is the escalation)

    Where does the replay corpus of stored sys_metadata overlay rows come from? Three shapes, and this is a product/data-access call, not a dev's:

    1. Real tenant data — is there a corpus we can lawfully replay against, and who can produce an export?
    2. Synthesised — generate overlay rows that exercise the seven EMPTY_COMBINATOR_SURFACES types. Cheap and available today, but it measures the generator's imagination, not the field.
    3. Ship behind the escape valve — land the gate defaulted off for object with OS_ALLOW_UNLINTED_METADATA_WRITES-shaped opt-in (D4's shape, already ruled on [runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463), collect real refusals from opted-in environments, then flip the default as a second card. This pays the budget with production traffic instead of a corpus.

    My reading, offered as input and not a recommendation to adopt: 3, because it is the only one that measures the actual field without needing a data-access answer first, and because it turns an unbounded precondition into a normal two-step rollout. But it does put a temporarily-declared-but-unenforced gate on object in the interim, which is an ADR-0049 tension worth your eye rather than mine.

    Three measured facts that should not be re-derived when this is next taken up

    Carried from #6715 via 5230446731, all measured on origin/main, none of them re-verified by me today — treat them as dated evidence, not current state:

    • The gate dispatches on flow alone across all six runtime-publish rules (6 of 37 rules reach that door). So this card is the whole frontier, and ⛔ must not be delivered as a one-line runtimeTypes edit — its own body says so.
    • The type list in circulation undercounts by three. EMPTY_COMBINATOR_SURFACES walks seven types (objects, views, pages, dashboards, reports, datasets, apps), not the "four" in the card and in the registry comment at authoring-rules.ts:502-510.
    • Three of the seven have a mechanical blocker: report / dataset / app have no TYPE_TO_STACK_KEY entry, and authoring-rule-wiring.test.ts fails on any gated type lacking one. So "one line" holds only for the four mapped types; opening the frontier means deciding what those three map to, or deciding deliberately that they stay out.

    Also: #6710 landed (30283263c), so the runtime publish gate is now active by default on every kernel rather than keyed on the environmentId proxy — more topologies reach this door than when the card was filed, which widens the frontier this card moves.

    ⇒ Suggested disposition (triage's call, not mine): pm:queue → needs-user-decision until the corpus question above is answered. A card whose own acceptance criterion is unpayable is not dispatchable, and leaving it pm:queue means it looks workable in every listing and consumes a reader each pass — the failure #8337 has been demonstrating for two days.


    Generated by Claude Code

  6. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    ContributorAuthor

    Premise verified: #4463 P1 has landed. This card's stated prerequisite is discharged.

    ⛔ Not a claim, not dispatched — recording a measurement and the two things that still stand between this card and a dispatch.

    The measurement

    This card is titled "…(#4463 P1 之后)", and I had it parked in this seat's working notes as 「前提待复验」 — for an entire shift, without ever verifying it. Read just now:

    #4463 — state: closed, state_reason: completed, closed 2026-08-02 by os-zhuang, closed_by_pull_requests: PR #4715 (MERGED) — feat(metadata-protocol,lint): 作者时规则接到运行时写入闸门 —— 第四扇门 (#4463)

    So the prerequisite was satisfied two weeks before the shift in which I parked this card as premise-unverified. The block was mine, not the repo's. Recorded as an error rather than quietly fixed: this card sat in pm:queue, dispatchable-in-principle, and was reported to the maintainer as part of a queue of zero. Full account at #6367 5308000846.

    What still stands before dispatch — two items, neither of them the premise

    1. The P2 scope is only 「建议」 in #4463, not ruled. #4463's batching note gives P2 as:

    registry 快照方案推广到全部 runtime-safe 规则;advisory findings 进响应,objectui 渲染(需要 objectui 侧一个配套 issue)

    The advisory-findings-in-response half is #4463's D3, and D3 was written as a recommendation (「建议」) alongside D1/D2/D4 — P1 landed without needing the advisory limb settled. Extending to all runtime-safe rules also re-opens D2: #4463 recommended option (c) "首期只按被写 item 的类型跑相关规则族" to start and option (a) "写入时从 registry 构造 stack 视图快照,跑全套" to finish — and this card is the (a) finish, whose per-save cost #4463 explicitly said needs measuring rather than assuming. That measurement has not been taken.

    2. The companion objectui card #4463 requires may not exist. ⚠️ Not checked. It is a prerequisite of the P2 scope by #4463's own text, and a rendering half with no card is the same evaporation shape that cost #9029 its home.

    Also worth flagging for whoever takes it

    packages/metadata-protocol/src/protocol.ts is a hot file and currently carries two in-flight claims this round (#9066 recordCommit, #9009 getEffectiveLock). A third concurrent editor sits at the standing cap, and this card's surface is far larger than either — region clearance would have to come from both PRs' actual changed ranges.

    ⛔ Labels and assignee untouched; the card remains pm:queue and unclaimed.


    Generated by Claude Code

  7. 10 remaining items

  8. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 4716,
      "status": "done",
      "branch": "claude/issue-4716-lazy-deps-tripwire-object-leg",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9295",
      "premise_still_valid": true,
      "summary": "Implemented the one ruled prerequisite and nothing else: runtime-lazy-deps.test.ts now gates an OBJECT write carrying a json_schema validation, in-process and in both spawned dist entries, so the assertion is as wide as its own docblock prose. Added a spawned positive control proving the require-cache probe really sees ajv + ajv-formats load when validateRuleCompilability judges that same body — a clean require-cache walk proves nothing until it has been shown to report dirty. Every PM 机制假设 was re-measured on this branch and all four held; none was falsified. Test-only: no rule, no AUTHORING_RULES entry, no runtimeTypes, no gate dispatch, nothing that changes what the gate accepts or refuses; the false-positive-budget precondition was not approached.",
      "tests": "All at head commit 0f405abc1 (branch built from origin/main @ fab693bee). (1) MEASUREMENT, spawned-process require.cache walk over built dist: import runtime.cjs -> 0/0; import full index.cjs barrel -> 0/0; RUN gate on flow write -> 0/0; RUN gate on OBJECT write carrying a json_schema validation -> 0/0; RUN validateRuleCompilability over that same object body -> ajv 63 modules + ajv-formats 3 modules. NON-VACUITY of the probe: that last run returns exactly 1 finding, validation-rule-json-schema-uncompilable ('schema is invalid: data/required must be array, ...'), cold 56.4 ms vs warm 11.5 ms. (2) REVERSE VERIFICATION, direction RED as predicted: ablated validateRuleCompilability to surfaces CLI_AND_RUNTIME + runtimeTypes ['object'] and REBUILT the package (the spawned legs read dist/, so an unrebuilt ablation would have run pre-mutation code and stayed green). Old guard from origin/main on that tree: 4/4 GREEN while ajv was demonstrably loading onto the boot path — the blind spot measured, not argued. New guard on the same tree: 3 of 6 legs RED (spawned CJS, spawned ESM, in-process object leg), each naming ajv; the flow leg stayed green, which is exactly why flow-only was blind. Ablation reverted, package REBUILT, and the artifact re-proved clean: runtimeAuthoringRulesFor('object') back to [validatePresetComparands, validateSecurityPosture], git diff HEAD empty. (3) SUITES: pnpm --filter @objectstack/lint test -> 73 files / 2067 tests passed; pnpm --filter @objectstack/lint typecheck -> tsc --noEmit clean; the guard file itself 6 passed (was 4). (4) GATE UNION re-derived at the final path set with node scripts/pm/dispatch-gates.mjs (not inherited from the prompt) and run at 0f405abc1, all green: check:nul-bytes (6066 files, no control bytes), check:cross-package-test-inputs, check:engine-double-contract (315 pinned / 133 debt / 2 exempt, unchanged), check:where-matcher (251 matchers, 0 new), check:query-options-erasure (baseline unchanged, no files added), check:type-check-coverage, scripts/docs-audit/check-affected-docs.mjs. check:type-check-debt's ratchet needs the whole workspace closure built; instead its subject was checked directly by reproducing its test-inclusive project for @objectstack/lint (package tsconfig with the test excludes dropped) — the edited file contributes 0 tsc diagnostics, so the ledgered count cannot drift up from this change. No TEST_DEBT entry raised or touched.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes the JSON cannot carry

    Every PM 机制假设 re-measured; all four held, none falsified.

    assumption re-measured on this branch
    ajv still loads on an object-write snapshot carrying a json_schema validation HOLDS — ajv 63 modules + ajv-formats 3, spawned require.cache walk, 1 finding, cold 56.4 ms / warm 11.5 ms
    the load is triggered by the WRITTEN TYPE, not the rule set HOLDS — and it is now the load-bearing reason the new leg exists; the flow leg stayed green under the ablation that reddened all three object legs
    runtimeGatedTypes() returns 8 types including object; 11 of 41 rules on runtime-publish HOLDS exactly — book, dashboard, flow, object, page, permission, seed, view; 11/41. The two rules gating object today are validatePresetComparands + validateSecurityPosture
    report/dataset/app have no TYPE_TO_STACK_KEY entry HOLDS — not touched; the table was not edited

    One deviation from the dispatch text, flagged rather than silently taken. The prompt said "Changeset per repo rules". The repo rule for this PR is no changeset: lint.yml:637 states that a PR which "releases nothing" is "the textbook skip-changeset case", and AGENTS.md:912 asks for a changeset on feature work only. This PR edits one test file and ships no published code. I wrote a changeset first, then removed it and applied skip-changeset instead. Labels read back after bots settled: size/m, tests, skip-changeset (the write was a union over what was already there, not a bare set).

    Part of #4716, never Fixes — the card's own acceptance criterion (the false-positive budget against a stored sys_metadata replay corpus) is unpaid and untouched, and the card must stay open.

    Two fixture properties a future editor can break silently, so both are guarded in-file rather than left to prose. The object body must keep tripping a rule that gates object today (else the dep probe is vacuous — asserted as rulesRun.length > 0 plus the filter-preset-comparand finding), and its schema must stay one ajv refuses (else the negative leg degrades into "a body with nothing in it loads nothing" — that is what the positive control fails on, with a message saying to restore the schema rather than delete the test).


    Generated by Claude Code

  9. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Adjudication record (maintainer, 2026-08-18, PM chat with the skills seat, session session_01WswfK2yNYT9hNnMH6TzAwL — analysis delivered in chat over this thread's full record, ruling given directly). Verbatim, untranslated: 「objectstack cloud 还没有正式上线,简化开发。」then, on the simplification package as proposed: 「同意」.

    Q1 (comment 5328455567) = B, exemption — with option A ruled structurally unavailable, not declined: objectstack cloud has not launched, so no real tenant exists whose sys_metadata overlay rows could be exported. That fact both closes route A and explains why the corpus was unfindable in-repo — it has not been produced by reality yet. The false-positive budget is exempted on today's measurement (0 refusals / 75 real object declarations across two authoring lineages, 0 phantoms after the instrument fix, non-vacuity proven by 6 synthetic violations refusing correctly), with the lower-bound caveat stated in as many words: every measured population is authored config-file metadata, the cleanest possible input. Post-launch follow-up expectation, not a gate: when the first real tenants exist, a replay of stored overlay rows is run as an after-the-fact audit of this exemption.

    Q2 dissolves by scope: this card's operative scope is the 5 gating rules of RUNTIME_OBJECT_WRITES_P2 only (the option-D arithmetic recorded in comment 5328455567). The 6 advisory-only rules do not ride — the ~8-advisories-per-write designer noise never materialises, and advisory UX becomes separate, non-blocking work.

    Split: the full runtime-safe snapshot half (RUNTIME_NEEDS_FULL_SNAPSHOT, the measured expensive half) leaves this card → filed as its own card (see follow-up issue filed today referencing this comment). The pre-existing linear cost of the shipped gate (33.4 ms @ 21 objects → 506 ms @ 420 objects per flow publish, measured in comment 5309093362) is filed as its own finding — it exists before and independent of this card.

    Label transition in the same stroke: needs-user-decision → pm:queue, unassigned — the domain:metadata seat dispatches at the narrowed scope (5 gating rules onto the object write door; lazy-deps tripwire prerequisite already landed via PR #9295; false-positive budget exempted per above).


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions