Skip to content

fix(docs,test): 文档失真修正与 ReDoS 断言去负载敏感 (#4485, #4476, #4486, #4452) - #4497

Merged
os-zhuang merged 7 commits into
mainfrom
claude/v17-verification-defects-gnf9e6-docs
Aug 1, 2026
Merged

fix(docs,test): 文档失真修正与 ReDoS 断言去负载敏感 (#4485, #4476, #4486, #4452)#4497
os-zhuang merged 7 commits into
mainfrom
claude/v17-verification-defects-gnf9e6-docs

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

v17 验收缺陷清单 #4482 的文档与 CI 稳定性批次。四项全部落地,CI 全绿。

已落地

#4485 —— ReDoS 断言去负载敏感(建议优先合并)

原断言用绝对 50ms 墙钟给病态输入设上界。这个上界量的是机器负载而不是解析器:全仓 pnpm test(约 130 个 turbo task 并行)下,健康代码也会超过 50ms,把完全没碰 @objectstack/metadata-core 的 PR 染红,诊断成本由当时正好在跑的人承担。

三条 toBeNull() 行为断言(真正的契约:病态输入是"无法识别"而非误拒)全部保留。被替换的只是那条墙钟代理指标,改为规模判据:同样的病态形状分别在 1x 和 8x 长度上测,断言解析开销随输入长度保持线性。负载在比值里基本抵消,这才是"对负载不敏感"的来源。

实测:健康解析随输入呈 8.3–8.5x,跨次稳定。40x 的上限留了约 5x 余量,同时仍能抓住哪怕只是二次的回归(约 64x),指数级回溯更是根本跑不完。

两处加固都是实测得出的,不是拍脑袋:

  • 两个计时在同一轮内背靠背取,并按比值取最小值,而不是各自独立取最小值再相除 —— 后者在调度抖动只落在其中一个窗口时会失真(在 3 倍 CPU 超订下实测复现过染红)。
  • 先预热 JIT,避免基线被拉高。

顺带一提:issue 里建议的"病态输入 / 良性输入"比值判据在这里不成立。良性的 16 字符 range 比 10 万字符的快约 300x,纯粹因为它短 10 万个字符 —— 那正是线性扩展的正常表现,该判据在健康机器上就会失败。所以采用了规模判据。

#4476 —— 版本号从 18 改回 17(实际覆盖 17 处,多于 issue 列的 9 处)

issue 列了 9 处,按它自己的建议改用裸模式全仓 grep,实际找到 17 处

位置 处数 说明
protocol/objectql/query-syntax.mdx 5 issue 列了 4 处;第 98 行 #4286 汇总段同样写错,issue 未列
data-modeling/queries.mdx 4 issue 所列
deployment/troubleshooting.mdx 1 issue 所列
skills/objectstack-query/SKILL.md 3 + rules/aggregation.md 1 + rules/pagination.md 1) 5 issue 未列,且是面向 agent 的,杀伤力最大 —— agent 写查询时把它当作事实来源
releases/implementation-status.mdx 2 issue 未列,同型错误但属于另一个变更 api.requireAuth#3963

判定 17 正确的证据(机器可读,非推断):spec-changes.jsondata.query.{cursor,distinct,joins,windowFunctions}stack.api.requireAuth 全部为 toMajor: 17;本树为 17.0.0-rc.1 / PROTOCOL_VERSION = '17.0.0',且这些键在 authorable-surface.json 里已是 [RETIRED] —— 一个变更不可能既在 17 的构建里已退役、又要到 18 才发布。

skills 只动正文、未动 frontmatter,因此 build-skill-docs.ts 从 frontmatter 派生的清单不受影响(check:skill-docs / check:skill-refs 已验证)。

按 issue 要求,把这个指纹补进了 docs/v17-docs-sweep.md:前两轮 sweep 只匹配接口名,所以径直读过了"接口名对、版本号错"的段落;新增行要求下一轮核对数字,并以 spec-changes.jsontoMajor 为准。

#4486 —— IDataEngine 读方法补回尾部 options

四个读方法都少了 options?: BaseEngineOptions。同一代码块里写方法各自带着 options,所以它教出来的恰好是反的心智模型("写有 options、读没有")—— 而这正是 #4251 要修的那个误解。

这个尾参不是可有可无:同一个 { context } 作为 insert 第 3 参正确,作为 find 第 3 参却被静默丢弃,于是本意为 isSystem 的绕过凭空消失(org-scoping hook 落地后控制面读取开始返回空)。照着这段文档写代码的人或 agent,会被引回 pre-#4251 的写法,而这个失败模式不报错

同时给代码块的 import 补上 BaseEngineOptions(真实契约从同一模块导入),并按契约原文记下优先级:query.context 仍受支持,两者同时给出时 options.context

#4452 —— service-automation README 按真实 flow DSL 重写

原 README 描述的 DSL 从未存在过:节点类型名全错(record_create vs create_recordquery vs get_record),插值方言是 Salesforce 的 {!…}(平台从不解析),分支/循环/错误处理写成嵌套 steps 数组(schema 里没有这个键)。照抄没有一个节点能跑。

#4414 + #4439 之后这不再只是"没用":conditions[].expression 进了表达式账本,README 的 '{!trigger.record.amount} > 10000' 现在会在 registerFlow() / objectstack validate 直接被拒 —— 而报错给的建议("去掉花括号")修不好,因为整个节点形状也是错的。

按 schema 和 executor 重写(不是照着错的版本改):

  • flow 是 nodes + edges 的 DAG。分支是,且 decision 靠把分支 label 与出边 label 对上来路由 —— A decision node has three declared ways to route a branch and two of them do nothing — app-crm's convert-lead guard runs both branches #4414 那个坑,已在示例里就地标注。
  • record-change 绑定在 start 节点的 config({ objectName, triggerType, condition }),不在 flow 顶层。
  • CRUD config 表:objectName(非 object)、filter对象(非 filters 三元组数组)、outputVariable(非 output)、没有 recordId。并注明 update_record 没有 outputVariable —— executor 不读。
  • 两套方言用一句话区分:条件一律裸 CEL,花括号只用于值。并明说 {!…} 不是这里的方言。
  • loop / parallel / try_catch 给出真实的 ADR-0031 region 形状(config.bodyconfig.branchesconfig.try/catch),wait 用节点级 waitEventConfig 而非臆造的 duration + nextSteps
  • flow type 值修正(schedule,非 scheduled)。
  • 补注 parallel / try_catch 虽不在 FlowNodeAction 枚举里,但确有注册的内建 executor —— 这正是 FlowNodeSchema.type活注册表而非闭合枚举校验的原因。

按 issue 的倾向,没有在这里复制一份逐节点参考:README 现在指向 content/docs/automation/flows.mdx(在维护的那份)。在包内 README 再维护一套节点清单,正是 #4027/#3569 那个"两份手写清单不对账"的形状,也就是这次漂移的来源。

验证

结果
CI(head eab5083,全部 5 个 workflow) ✅ 全绿(CI / Lint & Type Check / Docs Drift Check / Spec Liveness Check / PR Automation)
#4485 测试文件连跑 6/6 全绿(干净环境)
#4485 负载压测 8/8 全绿(3 倍 CPU 超订 —— 正是复现原故障的形状;修改前同条件下 5 次里失败 1 次)
@objectstack/metadata-core 全量测试 ✅ 8 文件 / 103 测试全绿(与 issue 给的基线一致)
pnpm --filter @objectstack/spec check:generated 8 项生成产物全部最新
pnpm typecheck(全仓) 122/122 task 成功
ESLint(改动的测试文件) ✅ 干净
#4476 九处逐行核对 ✅ 九处全部覆盖(按 issue 给的行号逐行验证),另加 8 处

changeset

本分支只动 .md/.mdx 与一个 .test.ts(已对 diff 核实:无包源码、无公开导出、无协议变更),因此用空 frontmatter 的 release-nothing changeset —— 它是"本 PR 不发布任何东西"的正式声明,而不是漏填。

是否可转正式

可以转正式评审。 四项均已完成、CI 全绿、验收标准(#4485 连跑)已满足。我按纪律没有自行转正式,也没有开 auto-merge,请按需翻转。

其中 #4485 值得优先合并:它落地后能减少其他 PR 因那条 50ms 墙钟断言而无端染红,属于对并行开发的即时止血。

需要评审者留意的范围判断

有两处我超出了 issue 的字面清单,均属"同型文档失真",若认为应拆分请指出:

  1. skills/objectstack-query/ 的 5 处 —— docs: nine passages date the #4286 query-surface removals to spec 18 — they ship in 17 #4476 的指纹清单未覆盖,但 issue 明确建议"用裸模式 grep 而不是逐文件改"。
  2. implementation-status.mdx 的 2 处 —— 同型错误,但属于 api.requireAuth把 public 从"全局开关的副产品"升级为声明式能力,然后删掉 api.requireAuth 开关 #3963)而非 [P2] data: QueryAST declares 12 members no executor runs — the liveness ledger governs metadata types, not the request surface #4286spec-changes.json 同样给出 toMajor: 17

未触及任何可作者化 spec key、公开导出或协议面,因此没有需要拍板的协议问题

分支:claude/v17-verification-defects-gnf9e6-docs

Fixes #4485
Fixes #4476
Fixes #4486
Fixes #4452

claude added 4 commits August 1, 2026 10:59
The ReDoS assertion in protocol-handshake.test.ts bounded the pathological
scan with an absolute 50ms wall clock. Under the full-repo run (~130 parallel
turbo tasks) that ceiling measures machine load rather than the parser: it
exceeded 50ms on a healthy tree and reddened PRs that never touched this
package, leaving the diagnosis cost to whoever happened to be running.

The underlying guard is real and stays (CodeQL 837/838). What changes is how
it is measured. The three `toBeNull()` behavioural assertions -- the actual
contract, that adversarial input is unrecognized rather than falsely rejected
-- are kept and now stand on their own. The wall-clock proxy is replaced by a
scaling check: the same adversarial shapes at 1x and 8x length, asserting the
parse stays linear in the input. Load largely cancels out of a ratio, which is
what makes the criterion load-insensitive.

Measured: healthy parsing tracks the input at 8.3-8.5x, stable across runs.
The 40x ceiling keeps ~5x headroom while still catching a merely quadratic
regression (~64x), let alone an exponential one, which would not finish.

Two measurement details are load-hardening, both established empirically:
timings are taken back-to-back within one iteration and reduced by minimum
*ratio* rather than minimising each timing independently (a scheduler steal
landing in only one window skewed the latter, observed reddening at 3x CPU
oversubscription); and the JIT is warmed so the baseline is not inflated.

Note the pathological-to-benign ratio suggested on the issue does not work
here: a benign 16-char range parses ~300x faster than a 100k-char one purely
because it is 100k characters shorter, so it would fail on a healthy machine.

Verification: 5/5 consecutive clean runs, plus 8/8 under 3x CPU
oversubscription (the shape that reproduced the original failure).

Fixes #4485
Seventeen passages dated v17 removals to `@objectstack/spec` 18. They ship in
17 -- this train. The number is the actionable half of a removal notice: a
reader on 16 asking whether upgrading to 17 breaks their cursor-paginated loop
was told the removal is a major away, so they plan for it later and the
upgrade breaks.

Evidence that 17 is correct: `spec-changes.json` carries `toMajor: 17` for
data.query.{cursor,distinct,joins,windowFunctions} and stack.api.requireAuth;
this tree is 17.0.0-rc.1 with `PROTOCOL_VERSION = '17.0.0'`; and the keys are
already `[RETIRED]` in `authorable-surface.json`. A removal cannot be retired
in a 17 build and also ship in 18.

#4476 fingerprinted nine locations. Grepping the bare pattern -- which the
issue itself recommended over working the list file-by-file -- found eight
more:

- The nine listed: query-syntax.mdx (4), queries.mdx (4), troubleshooting.mdx.
- query-syntax.mdx:98, the #4286 sweep summary paragraph, same error.
- skills/objectstack-query/ (5): SKILL.md and the aggregation/pagination rules.
  These are agent-facing and the highest-leverage of the set -- an agent
  authoring a query reads them as ground truth. Body prose only, so the
  frontmatter-derived listings that build-skill-docs.ts generates are unchanged.
- implementation-status.mdx (2): the same error shape on a different change,
  `api.requireAuth` (#3963), which spec-changes.json also puts at toMajor 17.

Also records the fingerprint in the sweep run log, as #4476 asks. Runs 1-2
matched on surface names and so read past passages that named the right
surface and the wrong release; the new row tells the next run to check the
number, with spec-changes.json `toMajor` as the arbiter.

Fixes #4476
The `IDataEngine` block in data-engine.mdx wrote all four read methods with
two parameters, dropping the trailing `options?: BaseEngineOptions` that the
real contract gives each of them (packages/spec/src/contracts/data-engine.ts
:70/85/89/90).

The write methods in the same block each carried their own `options`, so the
block taught exactly the wrong model -- "writes take options, reads do not" --
and that is the misconception #4251 existed to fix. The parameter is not
incidental: the same `{ context }` object is correct as insert's 3rd argument
but was SILENTLY DROPPED as find's, so an intended `isSystem` bypass vanished
and control-plane reads came back empty once org-scoping hooks landed. Anyone
-- human or agent -- writing code from this block was being led back to the
pre-#4251 shape, against a failure mode that raises no error.

Adds `BaseEngineOptions` to the block's import list (the contract imports it
from the same module), and a callout recording the precedence the contract
states: `query.context` remains supported, and when both are given
`options.context` wins.

`content/docs/kernel/contracts/` is hand-written -- only `content/docs/
references/` is generated -- so no generator run is involved.

Fixes #4486
#4452)

The README's flow sections described a DSL that has never existed. Every node
type name was wrong (`record_create` vs `create_record`, `query` vs
`get_record`), the interpolation dialect was Salesforce's `{!…}` which the
platform does not parse, and branching/looping/error handling were written as
nested `steps` arrays -- a shape the schema has no key for. Nothing in it ran.

Since #4414 + #4439 this stopped being merely useless: `conditions[].expression`
is on the expression ledger, so the README's `'{!trigger.record.amount} > 10000'`
is now REFUSED at `registerFlow()` / `objectstack validate`. An author copying
it got a CEL error whose advice ("drop the braces") did not fix their node,
because the node's entire shape was wrong too.

Rewritten from the schemas and executors, not from the old README:

- Flows are a DAG of flat `nodes` + `edges`. Branching is an edge, and a
  decision routes by matching its branch `label` to an out-edge `label` --
  the #4414 trap, called out inline.
- The record-change binding lives on the `start` node's config
  (`{ objectName, triggerType, condition }`), not at the flow top level.
- CRUD config table: `objectName` (not `object`), `filter` as an OBJECT (not a
  `filters` array of triples), `outputVariable` (not `output`), no `recordId`.
  Notes that `update_record` has no `outputVariable` -- the executor reads none.
- Both expression dialects stated with the rule that disambiguates them: every
  condition is bare CEL, braces are for values. Says plainly that `{!…}` is not
  a dialect here.
- `loop` / `parallel` / `try_catch` given their real ADR-0031 region shape
  (`config.body`, `config.branches`, `config.try`/`catch`), and `wait` its
  node-level `waitEventConfig` block rather than the invented `duration` +
  `nextSteps`.
- Flow `type` values corrected (`schedule`, not `scheduled`).

Per the issue's preference, the per-node reference is NOT duplicated here: the
README now points at content/docs/automation/flows.mdx, the maintained one.
Keeping a second hand-written node catalog in a package README is the
#4027/#3569 shape that produced this drift.

`pnpm --filter @objectstack/spec check:generated`: all 8 artifacts up to date.

Fixes #4452
@vercel

vercel Bot commented Aug 1, 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 1, 2026 12:13pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests size/l labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation.

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

  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation)

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.

…st fixes

The Check Changeset gate requires every PR to add at least one
`.changeset/*.md` relative to base. This branch touches only `.md`/`.mdx`
prose and one `.test.ts` file -- verified against the diff, no package source,
no public export, no protocol change -- so the empty-frontmatter form is the
accurate declaration: it publishes nothing.
claude added 2 commits August 1, 2026 12:13
…oo (#4452)

The Node Types section presented `FLOW_BUILTIN_NODE_TYPES` (i.e. the
`FlowNodeAction` enum) as the built-in set, but the ADR-0031 structured
constructs `parallel` and `try_catch` ship registered builtin executors
without appearing in that enum -- which is exactly why `FlowNodeSchema.type`
validates against the live action registry rather than a closed enum. Left as
written, the list contradicted the Advanced Features section just below it.
@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 12:56
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 2b64e0e Aug 1, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/v17-verification-defects-gnf9e6-docs branch August 1, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment