Skip to content

Commit a61dc86

Browse files
committed
Merge origin/main into claude/issue-4001-ui-batch14 (批 15 #5043 sync)
Ledger `ui/` section conflicted in four regions, as planned — 批 14 and 批 15 both edit it. Every row and both sides' prose kept; nothing resolved in favour of a side. - classification table: kept 批 14's fuller `no door` wording (it adds the call-site half of the measurement and names #4988/#5015) AND 批 15's new fifth class `no gate`. The two are NOT the same verdict: `no door` has no carrier key at all and points at ADR-0049 removal; `no gate` has a LIVE carrier (`<ObjectChart aggregate>`) with no parse between author and runtime and points at wiring the gate. Opposite follow-ups — collapsing them would prescribe the destructive action on one of the two. - class prose: both blocks kept (批 14's positive-control requirement and per-SCHEMA split finding; 批 15's no-door/no-gate table). - rows: auto-merged clean — 批 15 deleted `theme` and took `chart` 7 -> 2, 批 14 had deleted `action`/`report`/`dataset`/`dashboard` and taken `sharing` 2 -> 1. Header and subtotal RECOMPUTED FROM THE SURVIVING ROWS, not decremented: 29+20+9+2+7+6+4+4+4+3+1+1+1 = 91. Cross-checked from both directions (110 - 14 theme - 5 chart = 91; 100 - 3 dataset - 2 report - 2 dashboard - 1 action - 1 sharing = 91). 批 13 wrote 119, 批 14 wrote 110, 批 15 wrote 100 — each correct against its own branch, all three wrong against the merge, and 91 is a number no side ever wrote down. Seventh instance of the failure the automation/ section documents. Subtotal 65 of 91 authorable; 26 in the two no-parse classes (24 `no door`, 2 `no gate`). check:strictness-ledger arbitrates and is green. os-regen four-step: merged (never rebased), reinstalled + rebuilt spec, reset all seven generator-owned paths to origin/main, regenerated wholesale. Delta vs origin/main is exactly one file — content/docs/references/ui/sharing.mdx — so the regeneration reproduced main's artifacts byte-identically. Sibling coexistence asserted: 批 15's ui/theme.mdx and ui/chart.mdx both present and 0 changed lines against main, beside 批 14's sharing.mdx. Re-verified on the merged tree: spec 299 files / 7639 tests passed, tsc --noEmit clean, all sixteen gates green (8 generated + 8 source audits). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
2 parents 52723bf + 3af0354 commit a61dc86

44 files changed

Lines changed: 4862 additions & 417 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/plugin-audit": patch
3+
---
4+
5+
fix(plugin-audit): say where the audit system tables were provisioned, and stop skipping provisioning silently (#4887)
6+
7+
`AuditPlugin.provisionSystemTables()` created `sys_audit_log` / `sys_activity` /
8+
`sys_comment` at `kernel:ready` and then said **nothing** — not on success, and
9+
not when it skipped the work entirely (`typeof engine.syncObjectSchema !==
10+
'function'` returned silently). `syncObjectSchema()` itself returns `void` and
11+
has three silent exits of its own — the object is not in the registry, no driver
12+
resolves for it, or the resolved driver has no `syncSchema` — none of which
13+
throw. So "provisioned three tables" and "provisioned nothing at all" produced
14+
byte-identical logs, and the only way to tell them apart was to go looking in a
15+
database.
16+
17+
#4887 is what that costs. `sys_audit_log` and `sys_activity` were reported as
18+
never provisioned because they were absent from the primary SQLite file, with
19+
the silent `typeof` bail named as the likely cause. Neither was true:
20+
`sys_audit_log` (`lifecycle.class: 'audit'`) and `sys_activity`
21+
(`lifecycle.class: 'telemetry'`) are routed by **ADR-0057 §3.6** to the
22+
dedicated `telemetry` datasource whenever one is registered, and `os dev`
23+
registers one by default as a *sibling file* (`dev.db``dev.telemetry.db`).
24+
Both tables had been created — in the other store. `sys_comment` carries no
25+
lifecycle class, stays on the primary, and was the one that "existed". Nothing
26+
in the log connected those three facts.
27+
28+
Provisioning now reports itself:
29+
30+
- **Wholesale skip is a `warn`, naming the consequence** — the tables stay
31+
lazy-created on first WRITE, so an env that READS one first (the home page
32+
activity feed queries `sys_activity` before any mutation) logs "no such
33+
table" until something writes.
34+
- **One `info` line per boot listing where each table landed**
35+
`sys_audit_log→telemetry, sys_activity→telemetry, sys_comment→sqlite`,
36+
resolved through the engine's own `getDriverForObject`, so the log states the
37+
routing rather than leaving it to be inferred.
38+
- **A second `info` line when the ADR-0057 split is in effect**, saying
39+
explicitly that those tables live in a different store — on SQLite, a
40+
different *file* — and that anything reading them without naming the object
41+
(raw SQL against the default datasource) will report "no such table" even
42+
though provisioning succeeded.
43+
- **An object that resolves to no driver is a `warn`**`syncObjectSchema()`
44+
returns without issuing any DDL in that case and throws nothing, so the
45+
per-object `catch` never fires; from outside the engine this is the only place
46+
it can be observed.
47+
48+
Behaviour is otherwise unchanged: the same three objects are synced, per-object
49+
failures stay isolated, and an engine without on-demand DDL still degrades
50+
instead of failing `start()`.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
---
3+
4+
tooling: audit release-owned docs read-only instead of editing them (#4920)
5+
6+
`content/docs/releases/**` stays in the `docs-accuracy-audit` scope but is now reviewed
7+
**read-only**: those 9 pages get a review prompt that forbids edits and a finding schema
8+
with no `fixesApplied`, and their deliverable is a list of evidence-backed findings to
9+
file as issues. AGENTS.md marks the directory RELEASE-OWNED — release notes are compiled
10+
centrally at release time — while the audit's deliverable is an in-place mdx rewrite, so
11+
a full audit used to open exactly the PR that guardrail exists to stop.
12+
13+
Excluding them was rejected: it would leave the most-read pages permanently unaudited
14+
and add a second definition of the audit's scope next to the generated one (#4851).
15+
Silence was rejected too — a release page that produces no result, or whose agent admits
16+
it edited the file, fails the run by name, and the summary always carries
17+
`releases (read-only): N finding(s) — file issues, do not edit`, zero findings included.
18+
19+
`pnpm check:docs-audit-scope` now also anchors the prefix to AGENTS.md's guardrail row,
20+
requires release pages to still be in scope, and verifies the read-only fork by running
21+
the workflow against stub agents; the self-test mutates the fork away and requires that
22+
check to go red.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a re-registered driver stops crying wolf, and a real name collision starts saying what it cost (#4773)
6+
7+
Every boot printed one line into `⚠ Boot diagnostics`:
8+
9+
```
10+
WARN Driver already registered, skipping {"driverName":"com.objectstack.driver.sql"}
11+
```
12+
13+
It was never an anomaly. The standalone `default` datasource is registered
14+
twice, on two legs of one round trip, and traced end to end it is the **same
15+
object instance** both times:
16+
17+
1. `DatasourceConnectionService.attemptConnect()` builds the default driver and
18+
registers it (`isDefault: true`), driven by `DefaultDatasourcePlugin.init()`;
19+
2. that plugin republishes the instance it just read back out of the engine as
20+
the `driver.<name>` kernel service — the surface `os migrate` and serve's
21+
storage detection resolve the primary DB through — and
22+
`ObjectQLPlugin.start()`'s `driver.*` discovery loop bridges every such
23+
service into the engine, handing back the driver it already holds.
24+
25+
Nothing is decided and nothing is discarded, so `registerDriver` now reports
26+
that at `debug`. A no-anomaly line on every single boot does not belong at
27+
`warn`; it only teaches operators that `warn` means nothing.
28+
29+
The reason this is not a blanket downgrade: the same `warn` also covered the
30+
case that genuinely matters — **two different driver instances claiming one
31+
name**, where "skipping" silently drops one of two configurations (connection
32+
string, pool, tenant scoping, capability set) while every query bound to that
33+
name keeps working against the winner. The two are now told apart by object
34+
identity:
35+
36+
- **same instance**`debug`, nothing happened;
37+
- **different instance under a held name** → still `warn`, now naming which
38+
configuration was KEPT and which was DISCARDED (with both versions) so the
39+
operator can tell what is actually in force;
40+
- **same instance re-registered with `isDefault` while another driver holds
41+
that role**`warn`, because the caller's intent is otherwise dropped in
42+
silence.
43+
44+
Registration behaviour is unchanged in all three cases — first registration
45+
still wins. Only which of them is worth an operator's attention changed.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 清除 `validateOrgAxisRedLines` 里 spec 合法 stack 永远到不了的四条分支 (#5009)
6+
7+
`validate-org-axis-red-lines.ts``input: 'parsed'` 规则 —— 它看到的是
8+
`ObjectStackSchema` 解析后的产物。#4984 修掉了 sharing rule 字段那一层的 `??`
9+
别名读法,但同一文件里还留着四条同形分支,每一条读的键 spec 都不声明:
10+
11+
| 原读法 | spec 事实 | 处置 |
12+
|:--|:--|:--|
13+
| `cfg.permissions ?? cfg.permissionSets` | stack 根 **strip** 未声明键,`permissionSets` 解析后必为 `undefined` | 收敛为 `cfg.permissions` |
14+
| `cfg.sharingRules ?? cfg.sharing`(两处) | 同上 | 收敛为 `cfg.sharingRules` |
15+
| `str(rule.object ?? rule.objectName)` | `SharingRuleSchema``.strict()`,按名拒绝 `objectName`;`object` 又是必填 | 收敛为 `rule.object` |
16+
| `asArray(object.rowLevelSecurity ?? object.rls)` 整段(约 20 行) | **`ObjectSchema` 两个键都不声明**,且 `.strict()` —— 带对象级 RLS 的 stack 在 `os validate` / `os build` 直接被拒("Unrecognized key(s) on this object") | **删除** |
17+
18+
对任何 spec 合法的 stack,判定结果不变:这些分支本来就永远不执行(反向验证 ——
19+
新测试跑在改动前的实现上,29 条由 `safeParse` fixture 驱动的断言全绿)。真正的
20+
代价从来不是漏报,而是误导:对象级 RLS **根本不是授权面**(`authorable-surface.json`
21+
里只有 `security/PermissionSet:rowLevelSecurity` 一条),而那段死代码连
22+
`objects[N].rowLevelSecurity[M].using` 的诊断 path 都写好了,足以让下一位作者
23+
(人或 AI)相信它是真的并照着写更多代码 —— #5008 差点就这么做了。
24+
25+
行为上唯一的差别落在 `os lint`(不 parse,跑 normalized 层):把别名拼法写进
26+
stack 的作者,不再从这条红线拿到诊断,而是从 schema 那里拿到一条指名道姓的
27+
拒绝。别名容忍属于 producer 的拒绝,不属于 consumer(Prime Directive #12)。
28+
29+
同时补上一层结构性 meta-guard(#4992 模式),让下一条死分支在 review 前就红:
30+
31+
- **declared-key guard** —— 规则源码里从 stack / permission set / RLS policy /
32+
object / sharing rule 上读的每一个键,都必须出现在对应 schema 自己的 `.shape`
33+
里。扫源码而不是扫行为是刻意的:不可达分支根本没有行为可断言。
34+
- **reachability guard** —— 每个 `findings.push` 调用点都必须被至少一条过
35+
`safeParse` 的 fixture 触达;走不到的分支不允许存在。
36+
- 规则 ① 的 fixture 现在也走 `PermissionSetSchema.safeParse`(此前只有 sharing
37+
rule 和 object fixture 有这层保护)。
38+
39+
四条分支各自被变异测试验证过:把任意一条加回去,都至少有两条测试转红。
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
---
3+
4+
ci(deps): 给 OSV 门禁的逃生口立三条约定并机械强制 (#4965)
5+
6+
`validate-deps.yml` 的 OSV 门禁对无修复版本的公告,唯一出路是 `osv-scanner.toml`
7+
`[[IgnoredVulns]]`;但该文件此前不存在,逃生口一次没走过,也就没有约定 —— 而没有约定的
8+
逃生口会在第一次紧急情况下被随手用坏,然后永久留在那里。本次新增零豁免的
9+
`osv-scanner.toml`(三条约定写在文件头 + 一个注释掉的模板条目)与
10+
`scripts/check-osv-exemptions.mjs`:`ignoreUntil` 强制(默认 30 天、上限 90 天,缺失/
11+
加引号/过期/超上限一律判红),`reason` 强制且必须带 advisory 链接与一句"为什么不可修"。
12+
零豁免状态下门禁行为与本次改动前逐条一致。仅 CI 配置,不发布任何包。
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
---
3+
4+
docs(pm-dispatch,os-dev): 发现类 issue 纪律 —— 查重、归挂、`finding` 分级与分诊出水口 (#4949)
5+
6+
维护者观察「issue 越开发越多」后的复盘产物(cloud 分片一日数据:关 14 开 19)。
7+
病根不在 Prime Directive #10(发现即立单)—— 返工全部来自大而含糊/过时的
8+
issue,小而精确的单派发成功率近 100% —— 而在循环只有生产者、没有出水口。
9+
10+
- **os-dev 立单纪律三条**:立单前按关键词 + 文件路径查重(cloud#1054 与
11+
cloud#1031 同日重复);属已排队 issue 完成范围的开 sub-issue、仅依赖的
12+
独立立单 + `Blocked-by:`;观察类打 `finding` 标签、不自行判级压单
13+
(立单时判级最不准:cloud#1004 的「转义细节」实为 P0 过滤器旁路)。
14+
- **SKILL.md**:setup 增 `finding` 标签;step 0 分类增 Hold 类(评论/台账
15+
是 ADR-0049 的 silent state 与 rule 5 的 second tracker,记录必须留在
16+
issue 层);新增**发现分诊轮** —— 每 ~5 轮批量过一遍 `finding`,晋级 /
17+
关闭 not planned(否决窗口)/ 持有,判级责任归此轮;轮次报告增三个有界
18+
健康度指标(可派发库存、决策收件箱、`finding` 中位年龄),总 open 数
19+
刻意不设为指标。
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'create-objectstack': patch
3+
---
4+
5+
fix(create-objectstack): scaffolding a remote template no longer produces a project that cannot build (#4926)
6+
7+
`npx create-objectstack@latest my-app -t todo` (and `compliance`, `content`,
8+
`contracts`, `procurement`) generated a project that failed `objectstack build`
9+
immediately — 5 of the 6 offered templates. Only the bundled `blank` worked.
10+
11+
The scaffolder read the template's original namespace from
12+
`objectstack.manifest.json`, and that filename names two different documents.
13+
The bundled template's is app-shaped and carries `namespace`; a remote
14+
template's is the template-registry document
15+
(`$schema: …/template-manifest.json`) and carries none — its namespace lives
16+
only in `objectstack.config.ts`. So the value came back `undefined` for every
17+
remote template and the object-name rewrite was skipped, while the config's
18+
`namespace:` was rewritten anyway. The result was `namespace: 'my_app'` sitting
19+
next to `name: 'todo_task'`, which the `${namespace}_${shortName}` rule rejects.
20+
Across the five templates, 74 object names were left unrewritten.
21+
22+
`objectstack.config.ts` is now the authority for the template namespace (it
23+
holds the very literal the scaffolder overwrites, so the two cannot disagree),
24+
with the manifest as fallback. The rewrite also verifies itself: any surviving
25+
stale prefix throws at the scaffold, naming the files and lines, instead of
26+
surfacing as a build failure on the user's first command.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
**未知键收紧:`ui/theme.zod.ts` 全部 14 个站点 + `ui/chart.zod.ts` 5 / 7 个站点(#4001 批 15)**
6+
7+
作者写进主题和图表配置里的未声明键,过去被 zod 默认的 `.strip` 静默丢弃、解析照常成功。现在是一条点名该键、并在能识别时给出正确拼法的报错。
8+
9+
**先测门,再收紧。** 两个文件在账本上都标着 `authorable (p)`,`(p)` 是待验证:
10+
11+
- `theme.zod.ts` —— 门是真的,而且有两道:`stack.zod.ts` 声明 `themes: z.array(ThemeSchema)`(所以 `defineStack()` 在每次启动和 `objectstack build` 时解析每个主题),`defineTheme()` 直接解析一个。从 24 个 metadata-type root 加 `ObjectStackSchema` 做 BFS,文件内每个 schema 都可达。**14/14 收紧。**
12+
- `chart.zod.ts` —— 5 个可达(`DashboardWidget.chartConfig` / `ReportChartSchema`),已收紧;**`ChartAggregateSchema``ChartGroupBySchema` 的 object 分支不可达,故意保持原样**,见下。
13+
14+
## 破坏性变更 · 迁移
15+
16+
以下写法过去被静默接受(键被丢弃),现在报错。括号内是新报错直接给出的处方。
17+
18+
**主题(`defineStack({ themes })` / `defineTheme()`)**
19+
20+
| 旧写法 | 改成 |
21+
|---|---|
22+
| `colors: { card, foreground, mutedForeground, muted, destructive }` | `surface` / `text` / `textSecondary` / `disabled` / `error` |
23+
| `typography.fontSize: { md }` | `base`(`borderRadius`/`shadows``md`,字号阶梯没有) |
24+
| `typography.fontWeight: { base }` | `normal` |
25+
| `animation.timing: { easeIn, easeOut, easeInOut }` | `ease_in` / `ease_out` / `ease_in_out` |
26+
| `shadows: { inset }` | `inner` |
27+
| `zIndex: { backdrop, overlay }` | `modalBackdrop` |
28+
| 顶层 `palette` / `radius` / `shadow` / `animations` / `cssVars` / `extend` | `colors` / `borderRadius` / `shadows` / `animation` / `customVars` / `extends` |
29+
| #3494 删除的 8 个 prop(`spacing` / `breakpoints` / `logo` / `density` / `wcagContrast` / `rtl` / `touchTarget` / `keyboardNavigation`) | 各自带独立墓碑处方;多数指向 `customVars`,`logo` 指向 app 的 `branding.logo` |
30+
31+
**图表(dashboard widget 的 `chartConfig` / report 的 `chart`)**
32+
33+
| 旧写法 | 改成 |
34+
|---|---|
35+
| `chartType` | `type`(`chartType` 是内部拼写,从来不是作者契约) |
36+
| `legend` / `dataLabels` | `showLegend` / `showDataLabels` |
37+
| `interactions` / `annotation` | `interaction` / `annotations`(同一个块里一个单数一个复数) |
38+
| axis 上的 `name` / `label` / `dataKey` | `field` / `title` |
39+
| series 上的 `field` / `title` / `stackId` / `strokeDasharray` | `name` / `label` / `stack` / `dashArray` |
40+
| annotation 上的 `from` / `to` | `value` / `endValue` |
41+
| `interaction.zoom` / `interaction.clickAction`(#3752 已删) | `brush: true` / `onSegmentClick``ReportSchema.drilldown`、widget 的 `options`|
42+
| `width` / `stacked` / `dataset` / `objectName` / `aggregate` / `options` | 都是层级放错,报错点名正确的那一层(`layout.w``series[].stack`、widget 自己的键、react prop) |
43+
44+
⚠️ **严格性会顺着 `.extend()` 传到 `ReportChartSchema`**(`ChartConfigSchema.extend(...)`)。这是有意的,并有测试钉住:report chart 只是把 `xAxis`/`yAxis` 收窄成 dataset 维度/度量名,不新增键,所以继承的键集正好。
45+
46+
## 两个站点故意没收紧
47+
48+
`ChartAggregateSchema``ChartGroupBySchema` 的 object 分支**有活的承载键**(react 层 `< ObjectChart objectName aggregate={…} >`,objectui 的 `ObjectChart` 真的读它跑查询),**但没有任何 parse**:两者从所有 metadata-type root 都不可达,三个仓库里除单测外无人 `.parse()`,而唯一审查它的 react 页发布 lint 是手写重推规则、从不检查未知键。
49+
50+
`.strict()` 是 parse 的属性,这里没有 parse —— 收紧只会让文件看起来完成,并留下*一个被精确校验的死槽位*(#4583)。账本因此新增第四类 **`no gate`**(承载键活、parse 缺),与批 13 的 `no door`(承载键本身不存在)并列:两者处方相反,前者该接闸门,后者该走 ADR-0049 退役。已归档为独立 issue。
51+
52+
主题里那些**发出后无人读取**的 CSS 变量(`--font-size-*` / `--z-*` / `--duration-*` …)是 ADR-0049 的 liveness 题目,不是未知键题目,同样单独归档 —— 收紧能让被丢弃的键变响,不能让一个槽位变活。

0 commit comments

Comments
 (0)