Skip to content

feat(metadata-protocol,lint): 作者时规则接到运行时写入闸门 —— 第四扇门 (#4463) - #4715

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-4463-runtime-authoring-gate
Aug 2, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-4463-runtime-authoring-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4463

问题

#4409/#4445 把 26 条作者时规则收进一张 registry,os validate / os build / os lint 由构造保证跑同一套。三扇门都在 CLI 上。 元数据的运行时写入路径 —— Studio 编辑、REST /meta item CRUD、MCP/AI agent 授权 —— 最终都走 saveMetaItem,那里只做 per-type 的 Zod safeParse。26 条规则一条不跑。

对租户来说这不是「四扇门里最弱的一扇」,而是唯一的一扇:sys_metadata overlay 行根本不在 CLI 的 config 文件里,os lint 再完美也看不见,租户没有任何等价命令可跑。正文那个实测例 —— expression approver 是坏 CEL 的审批流(record.owner ==)—— 在 Zod 里 approver.value 就是个 string,schema 全绿,存进 sys_metadata,经 registerFlow 注册,触发时节点在入口处失败。这正是 #4409 之后 os lint 一直在拦的那个 body。

它同时是 AI 作者走的那扇门,这也是裁决里权重最高的一条:模型写出来的元数据恰恰最容易出错,却从唯一没有检查的入口进来。

做法:一个共享校验核 + 一个运行时闸门

不是四套规则引擎,也不是每个 surface 各写一遍。

  1. 共享核落在 packages/lint。 AUTHORING_RULES 从 packages/cli/src/lint/ 搬到 packages/lint/src/,连同五个原本住在 CLI 的规则模块(lintFlowPatterns / lintLivenessProperties / lintAutonumberFormats / lintViewRefs / data-model-rules)—— 规则逻辑零改动,只是搬家。CLI 改为调用同一核(裁决第 3 条),packages/spec/** 零改动。

  2. 依赖障碍是解决的,不是绕开的。 新增子路径导出 @objectstack/lint/runtime,metadata-protocol 只 import 这个入口。新加的 runtime-lazy-deps.test.ts 钉住比 lazy-deps.test.ts 更强的一条:不只是 import 时不加载,跑闸门判一个真实 flow body 时也不加载 typescript / sucrase。lazy-deps.test.ts 本身一行未改、仍绿。

  3. surfaces 维度是 P1 的结构性最小集,不是顺手做的 P3。 棘轮守卫要覆盖新 surface(正文说这条「不是建议,是这个 issue 存在的理由」),而守卫无法守一个没被声明的 surface —— 否则运行时门就只能硬编码一份规则名单,正是本单要禁止的「手工接线第四命令」。所以每条规则新增 surfaces + (runtimeTypes | surfaceReason)。P3 的其余部分(推广成 N-surface、docs 表格第四列)不在本 PR。

落地行为

决策 实现
D1 只挡 state: 'active'。draft 保存永远放行。publishMetaItem 的 draft→active 提升也走同一闸门 —— 否则 ?mode=draft + POST /publish 就是免费绕过,而这正是 Studio designer 每次编辑做的事
D2 选项 (c):按被写 item 的类型建每次写入的 stack 快照,首期跑 flow / approval / expression / reference 四族
D3 gating → 422 INVALID_METADATA,复用 Zod 失败已有的结构化 issues 信封,每条带 rule / path / where / message / hint。objectui 不需要新协议
D4 只挡新写入,读路径不拒绝。逃生阀 OS_ALLOW_UNLINTED_METADATA_WRITES=1,置位后放行且每次都完整打日志

差分求值是 D4 的结构化实现,不是防御性补丁:规则跑两遍(上下文含/不含被写 item),只有这次写入 新增 的 finding 才能拒绝它。否则保存 flow A 会因为一条早就存在、可能是包里发来的坏 object 谓词而 422 —— 那比本单要补的洞更糟。

降级放行:逐条列出

裁决允许「个别规则在运行时上下文缺输入则降级为 warn」,要求显式记录。本 PR 没有任何规则被降级为 warn。 采取的是另一种显式收窄:每条不上运行时门的规则在 registry 里写明理由,三类共享常量:

  • RUNTIME_NEEDS_FULL_SNAPSHOT —— 规则读的是快照没带的全栈集合(pages / dashboards / nav / translations / seeds)。运行时宇宙其实更全(D2 说的那个反直觉红利),但构造那个快照是 P2;拿半个快照跑,会把租户没放进这次写入的元数据报成「缺失」。
  • RUNTIME_HEAVY_SOURCE_PARSE —— 规则要用 typescript / sucrase 解析作者写的源码(react/jsx 页面、L2 JS body)。这两个依赖正是 kernel boot path 不能碰的;Studio 的页面编辑另有 save-time 编译路径。
  • RUNTIME_OBJECT_WRITES_P2 —— 规则判的是 object/field 声明。object 写入是产品里最热的元数据路径(Studio 每次改字段),一个误判 422 的爆炸半径是整个产品,所以 P1 先上 flow(issue 的实测例和全部验收标准都是 flow),等闸门跑过真实流量再放宽。

两条单独写了具体理由,因为它们不是「以后再说」:

  • validateCapabilityReferences —— 唯一一条运行时宇宙让它更严的规则(「可能由别的包提供」这个 advisory 对冲在活的 capability registry 面前是可判定的)。那是 advisory→gating 的严重性变更,该单独一个 PR,不该搭在接线 PR 上。
  • validateSecurityPosture —— 这个 surface 上已经被另一套机制挡住了:plugin-security 通过 registerAuthoringGate 在 object 上跑同一套 OWD posture 规则。再跑一遍 linter 会把一次拒绝用两种话术报两遍。合并到本表是 P2,是合并不是缺口。

验收证据

1. 实测例被 422 拒绝,带结构化 issues

[invalid_metadata] flow/leave_approval failed author-time validation:
  flows[0].nodes[1].config.approvers[0].value: [approval-expression-invalid]
  expression approver does not parse as CEL: Unexpected token: EOF
status 422 · code INVALID_METADATA · rulesRun 含 validateApprovalApprovers

2. 同一份元数据 draft 保存放行 / 3. 逃生阀置位后放行且日志响亮 —— protocol.runtime-authoring-gate.test.ts 9 passed。

4. CLI 三命令与运行时门跑的是同一个模块 —— 用「删一条规则,两侧同时红」证明

把 validateApprovalApprovers 从 AUTHORING_RULES 删掉一条(不动任何其他文件):

--- SIDE A: 共享核侧 packages/lint ---
 x the runtime gate and the CLI commands read the SAME array
 x every rule @objectstack/lint exports is wired into a registry
 x the registry is non-empty and still holds the rules that motivated it
 x rejects the issue's worked example with a located, fixable finding
 x runs the four rule families #4463 P1 wired, from the shared table
--- SIDE B: 运行时门侧 packages/metadata-protocol ---
 x refuses an ACTIVE save of the broken approval flow with a 422
 x gates the draft->active PROMOTION, so the draft door is not a bypass
 x OS_ALLOW_UNLINTED_METADATA_WRITES=1 allows the write and says so loudly
 x survives a host whose registry cannot list objects

一次删除,两侧同时红。

5. lazy-deps.test.ts 仍绿且未被放宽 —— 该文件 diff 为空;新增的 runtime-lazy-deps.test.ts 是更强的一条,不是替代。

6. 棘轮守卫覆盖新 surface,且能 sabotage 验证 —— 两个反向注入都被抓住:

# 注入 A:运行时门直接手工接线一条规则
 x the runtime gate consumes the registry and names no rule of its own
   -> packages/metadata-protocol/src/runtime-authoring-gate.ts
      calls registry rule(s) directly: validateApprovalApprovers.

# 注入 B:第四扇门根本不接核
 x the runtime gate consumes the registry and names no rule of its own
   -> must run the shared core: expected ... to match /runRuntimeAuthoringRules\s*\(/

测试

packages/lint              Test Files  53 passed (53)   Tests  928 passed (928)
packages/metadata-protocol Test Files  27 passed (27)   Tests  225 passed (225)
packages/cli               Test Files  65 passed (65)   Tests  581 passed (581)
typecheck (lint, cli)      Done
check-type-check-coverage  OK  (metadata-protocol 原始 tsc 错误数 59 -> 59,零增量)
spec check:generated       All 8 generated artifacts are up to date

已合入 origin/main 后重建 spec 并复跑上述三包。

明确不在本单

  • P2:advisory findings 进响应 + objectui 渲染;registry 快照推广到全部 runtime-safe 规则;object 等类型上门。本 PR 里 advisory findings 走去重后的服务端日志,不进响应 —— 跑了规则却把结论丢掉,是本单要修的那个洞小一号的版本。
  • P3:surfaces 推广成通用 N-surface 维度 + docs 「the gate」表格第四列。

🤖 Generated with Claude Code

https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny


Generated by Claude Code

…e publish gate (#4463)

The 26 author-time rules #4409 collected behind one registry ran on `os validate`,
`os build` and `os lint` — three CLI commands. Every runtime metadata write (Studio,
REST /meta CRUD, MCP/AI authoring) reaches `saveMetaItem`, which did a per-type Zod
`safeParse` and stopped, so none of the 26 ran there. For a tenant that was not the
weakest of four doors but the only one: a sys_metadata overlay row is not in the
CLI's config file, so no command could have caught it.

Shape follows the ruling: one shared core, one runtime gate.

- Move AUTHORING_RULES (and the five rule modules that lived beside it) from
  packages/cli into @objectstack/lint; the CLI now calls the same table.
- Add the kernel-safe subpath entry @objectstack/lint/runtime, guarded by a new
  runtime-lazy-deps.test.ts: neither typescript nor sucrase loads at import or
  while gating. lazy-deps.test.ts is untouched.
- Declare `surfaces` + `runtimeTypes` / `surfaceReason` per rule, and extend the
  #4445 ratchet to the new surface: a rule that answers neither fails, a gate that
  hand-wires a rule fails, a gate that does not run the core fails.
- Gate `state: 'active'` saves and the draft->active promotion for `flow` with the
  flow/approval/expression/reference families; 422 in the existing structured-issues
  envelope. Drafts pass (D1). Evaluation is differential, so only the submitted item
  can refuse its own write and stored rows are never re-judged (D4).
- Escape hatch OS_ALLOW_UNLINTED_METADATA_WRITES=1, loud on every use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@vercel

vercel Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 9:48pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/metadata-protocol.

24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, @objectstack/lint)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

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.

claude added 2 commits August 2, 2026 21:05
…sh repo double (#4463)

Two CI failures on the first push, both real.

1. `check:nul-bytes` — the differential evaluator's finding fingerprint used
   three BARE NUL bytes as its separator. Runtime behaviour was fine; the file
   was not: grep/ripgrep classify a file containing a raw NUL as binary and
   return zero matches for it, silently, so the module disappears from code
   search and every grep-driven lint. git never warned because it sniffs only
   the first 8000 bytes and this NUL sat at byte 5361. Particularly bad here —
   the ratchet this PR adds is itself grep-driven, and the vanishing file was
   in the package it scans. Now the escaped U+0000 form: byte-identical at
   runtime, greppable at rest. (The shorter escape is deliberately avoided —
   a following digit turns it into a legacy octal escape.) Matches the
   existing convention at packages/rest/src/rest-server.ts:1065.

2. `Test Core` — objectql's publish double stubbed `getOverlayRepo` with
   `promoteDraft` alone, so `promoteDraftForPublish` reading the pending draft
   for the gate failed with `repo.get is not a function`. The real
   `SysMetadataRepository` has `get`; the double was narrower than the contract
   it stands in for (#4550's shape), so the double is what is wrong. Widened
   it. The gate is NOT made conditional on the method existing — a repo-shaped
   capability sniff would silently reopen the `?mode=draft` + `POST /publish`
   bypass this PR closes, which is worse than the TypeError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
…thoring gate (#4463)

Found while auditing every internal `saveMetaItem` caller, not from a red
build — `migrateStoredMetadata` re-saves rows that ALREADY EXIST with
`mode:'publish'`, so the new gate refused any stored row carrying a violation.

That inverts the tool and contradicts D4. The migration's job is to rewrite a
stored body into the current dialect; it is not an author publishing anything.
Refusing meant a tenant holding one pre-existing bad flow could never
canonicalize that row: the migration reported `outcome:'failed'` and left the
body in the OLDER dialect — worse than the state it was asked to improve, and
precisely the "存量行走 ADR-0087 的老路" D4 promises.

Carve-out is keyed on `source === 'migrate-stored'`, which is stated by the
SERVER and never forwarded from a request (see the provenance note at the top
of `saveMetaItem`), so no caller can spell its way past the gate — pinned by a
test that walks four near-miss spellings and expects 422 for each.

`duplicatePackage` is deliberately NOT exempt: it mints brand-new rows under
new names, and a copy of a broken flow is a new broken flow. Its per-row
failure reporting already surfaces that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny

Copy link
Copy Markdown
Contributor Author

复核后三处改动(第二、三次推送)

1. check:nul-bytes 红 —— 已修

fingerprint 分隔符用了三个裸 NUL 字节。运行时没错,文件有错:含裸 NUL 的文件会被 grep/ripgrep 判为 binary 并静默返回零匹配,模块从代码搜索和所有 grep 驱动的 lint 里消失。git 不报警是因为它只嗅探前 8000 字节,而这个 NUL 在第 5361 字节。

对本 PR 尤其讽刺:新加的棘轮守卫自己就是 grep 驱动的,而消失的文件正在它要扫的包里。修复前 grep -n fingerprint packages/lint/src/runtime-gate.ts 的输出就是 binary file matches,修复后正常返回三行。

改成转义形式(运行时字节完全等价),沿用 packages/rest/src/rest-server.ts:1065 的既有惯例;短转义按提示避开(后跟数字会变成 legacy octal escape)。

check-nul-bytes: OK (2938 tracked source file(s), no raw NUL bytes).

顺带扫了本 PR 触碰的全部 31 个文件,无第二处。(写 commit message 时我自己又不小心带进去一个,已在提交前查出并清掉 —— 这个坑比看起来容易踩。)

2. Test Core 的 objectql 回归 —— 已修,修的是替身不是闸门

protocol-publish-package-drafts.test.ts 的 makePublishable 把 getOverlayRepo 替身成只有 promoteDraft 的对象,而 promoteDraftForPublish 为了跑闸门要先读 pending draft,于是 repo.get is not a function。

真实的 SysMetadataRepository 有 get —— 替身比它所替代的契约窄,这是 #4550 的形状,所以错的是替身,补齐替身。

没有用 repo.get?.() / typeof repo.get === 'function' 之类的能力嗅探消红:那会让 draft→active 闸门在任何 repo 不完整的宿主上静默失效,等于把本 PR 刚关掉的后门无声地重新打开,比 TypeError 糟得多。

✓ protocol.publishMetaItem — seed self-apply > applies the seed body on publish and reports seedApplied
✓ protocol.publishMetaItem — seed self-apply > suppresses the self-apply when _skipSeedApply is set (package batch path)
✓ protocol.publishMetaItem — seed self-apply > does not touch the loader for non-seed publishes
Test Files  1 passed (1)      Tests  16 passed (16)

3. 主动发现并修掉的第三处:os migrate meta --stored 被闸门误伤(D4 回归)

不是红的 CI 报出来的,是我按「改 protocol.ts 的爆炸半径包含跨包消费者」这条教训,回头把每一个内部 saveMetaItem 调用点都过了一遍发现的。

migrateStoredMetadata(os migrate meta --stored --apply)用 mode: 'publish' 重存已经存在的行。闸门会把任何带违规的存量行拒掉 —— 这把工具反转了,也和 D4 直接冲突:租户只要有一条早年写下的坏 flow,那一行就永远无法被规范化,迁移报 outcome: 'failed' 并把 body 留在更旧的方言上,比它被要求改善的状态还差。这正是「存量走 ADR-0087 的老路」要防的。

修法:按 source === 'migrate-stored' 豁免。安全,因为 source 是服务端声明的、从不从请求转发(saveMetaItem 顶部的 provenance 注释写明了这点),调用方无法拼出这个词绕过闸门 —— 另加一条测试走四种近似拼法,每种都必须 422。

duplicatePackage 故意不豁免:它是用新名字铸造全新的行,一份坏 flow 的拷贝就是一条新的坏 flow;它本来就按行报 failure。

本地全量验证(四个跨包消费者)

packages/lint              Test Files  53 passed (53)    Tests   928 passed (928)
packages/metadata-protocol Test Files  27 passed (27)    Tests   227 passed (227)
packages/objectql          Test Files 103 passed (103)   Tests  1638 passed (1638)   ← 上轮 1 failed / 3 failed
packages/cli               Test Files  65 passed (65)    Tests   581 passed (581)
check:nul-bytes            OK          pnpm lint (eslint) 干净
typecheck (lint, cli)      Done        check:adr-anchors  OK (17 anchored files)
check-changeset-fixed      ✓           pnpm install --frozen-lockfile  OK

另外为了确认闸门不会误伤仓内既有元数据,离线把仓库里所有 flow 形状的 JSON body 灌进 runRuntimeAuthoringRules 跑了一遍:0 条会被拒。CI 的 Dogfood Verify CLI / Dogfood Regression Gate 也全绿,与之一致。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 21:23
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

已跟上 main(b8cbc57d0),仅做这一件事

MERGE_CONFLICT 弹出后重建 worktree,把 origin/main(ad047d2ee)merge(不是 rebase —— 四个提交已过复核,rebase 会打乱审阅历史)进分支。现在 git rev-list --count HEAD..origin/main = 0。

文本冲突:零。 git diff --name-only --diff-filter=U 为空,pnpm-lock.yaml 未被合并触碰,没有任何生成物需要重新生成。队列那次 MERGE_CONFLICT 与本地实际合并结果不一致,应是推测性合并的产物。

但 #4718 确实有语义影响,而且正落在本 PR 搬动的文件上

#4718(移除 app.areas[].visible / requiredPermissions)修改了 packages/cli/src/utils/lint-liveness-properties.test.ts(+58 行)—— 正是本 PR 搬去 packages/lint/src/ 的五个模块之一的测试。

这本该是一次 modify/delete 冲突;没有冲突,是因为 git 的重命名检测跟随了这次移动,把 #4718 的修改干净地应用到了新路径。已逐条核实两个新用例落在新位置且真的在跑:

packages/lint/src/lint-liveness-properties.test.ts:273
  it('is silent on the fail-open area gates — retired in 17.0.0 (#4651)')
packages/lint/src/lint-liveness-properties.test.ts:301
  it('the area-gate silence is a real verdict, not a lint that stopped loading ledgers')

packages/lint 的用例数 928 → 929(其中一个替换了旧断言,一个新增),数字本身就是「#4718 的断言在新路径上跑起来了」的证据 —— 若重命名跟随失败,它们会连同旧文件一起消失,而两边都不会报错。

另按提示自查了两个被删的键在本 PR 触碰的包里的出现位置:除上面这个被搬动的测试外,其余全部命中 validate-capability-references.* / validate-action-name-refs.ts / CHANGELOG.md —— 都是 packages/lint 里本 PR 未搬动、未修改的既有文件,main 上已与 #4718 自洽。packages/metadata-protocol 零命中。

合并后全量复验(四个包,不只受影响的那个)

packages/lint              Test Files  53 passed (53)    Tests   929 passed (929)   ← 928→929
packages/metadata-protocol Test Files  27 passed (27)    Tests   227 passed (227)
packages/objectql          Test Files 103 passed (103)   Tests  1638 passed (1638)
packages/cli               Test Files  65 passed (65)    Tests   581 passed (581)

typecheck (lint, cli)      Done
pnpm lint (eslint)         clean
check:nul-bytes            OK (2940 tracked source files, no raw NUL)
check:adr-anchors          OK (17 anchored files)
check:type-check-coverage  OK
pnpm install --frozen-lockfile   OK(锁文件未变)
spec build + check:generated     ✓ All 8 generated artifacts are up to date

合并前重建了 packages/spec(它在 main 侧动过)再跑上述全部。

范围未变:三条已复核的裁定(surfaces per-rule 声明、P1 只挡 flow、migrate-stored 豁免)一字未动,本次只有一个 merge commit。等 CI 全绿后由 PM 重新入队,我不自行 merge、不自行入队。


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ry findings on the save response (objectstack-ai#4717) (objectstack-ai#7435)

objectstack-ai#4463 put the shared author-time rule registry on the runtime write path — the
fourth door, and for a Studio tenant or an MCP/AI author the ONLY one. It gated
on `error` findings only; the advisory half was produced, walked into a
`console.warn` deduped once per process, and dropped. PR objectstack-ai#4715 named that when
it shipped: running a rule and discarding its conclusion is a smaller version of
the hole the gate exists to close.

The case is reachable, not theoretical. A flow whose only defect is a
`delete_record` node declaring `multi: true` with no `filter` yields
`errors = 0 / advisories = 1`: the write succeeds, the row persists, and the
author never learns their nightly sweep deletes every row on every run.

- `RuntimeAuthoringIssueSchema` declares the finding shape ONCE, in spec —
  the same `rule`/`path`/`where`/`message`/`hint`/`severity` the 422 envelope
  carries (objectstack-ai#4463 D3). `metadata-protocol` re-exports it as its
  `RuntimeAuthoringIssue` instead of keeping a parallel hand-written interface.
- `SaveMetaItemResponseSchema.advisories` — optional, array of that element.
- `evaluateRuntimeAuthoringGate` returns a `RuntimeAuthoringVerdict`
  (`{ error, advisories }`) instead of `Error | null`. An ADDED return channel:
  the success path previously returned `null` and had nowhere to put a verdict.
- `saveMetaItem` attaches the advisories, emitting the key ONLY when non-empty —
  never `[]` — so a clean save's response bytes are unchanged.

`rulesRun` stays off: the gate appends a gate-local rule id for `flow`, so not
every id it would list resolves in the lint registry. Save door only; the
publish door's asymmetry is stated in the changeset.

Fixes objectstack-ai#4717

Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants