Skip to content

Commit d12940e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16173-shard-timings-stale-cli
2 parents 2ddbab8 + cbca47d commit d12940e

27 files changed

Lines changed: 1757 additions & 157 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
`attestFreshDatastore` looks its `os migrate` remedy up instead of defaulting it
6+
7+
When a fresh datastore's own boot has already admitted a value that contradicts a
8+
migration's contract, that id is not attested and the operator is told what closes
9+
the gate on real evidence. The sentence used to be built from a two-way branch: the
10+
file-references id got `files-to-references`, and **every other id** got
11+
`value-shapes` by default.
12+
13+
`CREATION_ATTESTED_MIGRATION_IDS` has three members. For the third —
14+
`adr-0030-notification-event` — that default is a wrong prescription: `os migrate
15+
value-shapes --apply` neither attests nor clears it, and there is no `os migrate
16+
notification-event` sub-command to send an operator to at all (that cut-over is an
17+
operator call with no self-check).
18+
19+
The branch is now an explicit id-to-remedy register, total over the ids a
20+
value-shape tally can contradict. The loop asks it rather than falling into an arm,
21+
so an id with no value-shape contract is never-contradictable by that evidence and
22+
is attested on the birth observation as before. A new member therefore inherits no
23+
remedy: adding a third arm that happened to be right today would only have moved the
24+
same defect onto the fourth member.
25+
26+
No behaviour changes for the two ADR-0104 ids, which is where every reachable path
27+
runs today: the shipped engine keys its admitted-violation tally from a closed
28+
`'media' | 'value-shape'` union, so it cannot name a third id.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`check:api-surface` (and every other gate that reads `packages/spec/dist`) no longer refuses a dist that is exactly current because a source file's mtime moved without its bytes changing.
6+
7+
The freshness rule shared by four gates and the pre-commit hook compares `dist/**/*.d.ts` mtimes against `src/**/*.ts` mtimes. That is the right primitive — it is the artifact those gates consume, and it sees the hand-edited dist and the toolchain change no content digest can — but it cannot tell a real edit from a rewrite that left the bytes alone. A `git merge` re-checks-out an unchanged source file and bumps its mtime; the build that follows correctly does not run, because turbo's cache hashes content, so it is a cache hit that rewrites nothing and leaves every `dist/` mtime where the previous build left it. The gate then refused a correct dist, and prescribed a full rebuild — minutes, under the shared verify lock — of an artifact that needed none.
8+
9+
The mtime rule keeps its power to convict and gains one way to be answered. `packages/spec`'s build now records a second stamp beside the existing one, `dist/.build-input-hash-dts`, holding the same build-input digest — but written **only** by a build that actually emitted declarations, so `OS_SKIP_DTS=1` leaves it alone. When that digest equals the sources on disk, the declarations demonstrably describe them and the refusal is cleared. The evidence may only ever **acquit**: a missing, unreadable or mismatched stamp leaves the mtime verdict standing, so nothing that passed before can start failing, and the `OS_SKIP_DTS=1`-on-a-built-tree shape that ruled out `dist/.build-input-hash` for this purpose still fails, because that build never refreshes the new file.
10+
11+
The refusal message was wrong in the same case and is now driven by what was measured: it names a real content change and prints both digests when the stamp disagrees, says plainly that there is nothing to compare against when no stamp exists, and no longer sends every reader after `OS_SKIP_DTS` regardless of cause. It also notes that a repo-wide `pnpm build` may be a cache hit that rewrites nothing, so the remedy names the package build directly.
12+
13+
The published tarball gains one 65-byte file next to the stamp it already shipped.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
fix(platform-objects): the es-ES and ja-JP `dashboard.gap` help text says what its source now says
6+
7+
`metadataForms.dashboard.fields.gap.helpText` read `Separación de cuadrícula (unidades
8+
Tailwind)` in es-ES and 「グリッド間隔(Tailwind 単位)」 in ja-JP. Both were faithful
9+
translations of the source they were extracted against, `Grid gap (Tailwind units)` — but
10+
that source has since been rewritten to `Space between widgets, in steps of 0.25rem
11+
(4 = 1rem)`, which deliberately drops the CSS framework unit an app author never chose and
12+
cannot act on, and adds the magnitude the author can size a dashboard with.
13+
14+
Both leaves kept the retired vocabulary and never gained the magnitude, because bundle
15+
merge fills gaps only: a present-but-stale leaf is not a gap, so no amount of
16+
re-extraction corrects it. They now read `Espacio entre widgets, en incrementos de 0.25rem
17+
(4 = 1rem)` and 「ウィジェット間の間隔、0.25rem 刻み(4 = 1rem)」 — the grid framing is gone
18+
exactly as it is upstream, `widgets` / 「ウィジェット」 is the word each bundle already uses
19+
for dashboard widgets, and the conversion is carried so a Spanish- or Japanese-reading
20+
author can size `gap` without reading the English.
21+
22+
Two leaves. `columns` is unchanged upstream, so `Columnas de cuadrícula (predeterminado
23+
12)` and 「グリッド列(既定 12)」 stay accurate, and the other 13 source-derived prose leaves
24+
of this subtree (5 section descriptions plus 8 further field help texts) were read against
25+
the current English and are accurate in both locales.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os i18n extract --no-metadata-forms` is honoured whatever `--objects-only` is set to, and the Studio metadata-form baseline lands in exactly one module.
6+
7+
The flag gated only the `<locale>.metadata-forms.generated.ts` companion. The stack module's renderer had a third mode, `kind: 'full'`, that serialised the WHOLE `TranslationData` — the baseline included — and `--no-objects-only` selected it. So the two flags stopped being independent the moment the second one was passed, in both directions:
8+
9+
- **`--no-metadata-forms --no-objects-only`** suppressed the companion and wrote the same keys into `<locale>.objects.generated.ts` instead. Driven on a one-object, one-app stack with `i18n.defaultLocale: 'zh-CN'`: the emitted zh-CN module carried **776 leaves, of which 773 were the metadata-form baseline** the flag had just switched off (the stack's own surface is 3). Those 773 are **English** — the default locale is filled from the source labels and the metadata-form registry authors them in English — so a non-English default locale shipped the platform's English Studio strings inside its own application bundle.
10+
- **`--no-objects-only` alone** wrote those 773 keys **twice**, once in each module.
11+
12+
`--objects-only` picks the stack module's sub-tree; `--metadata-forms` decides whether the baseline is emitted at all, and it is now the only control over it **on both faces**. Both flags keep exactly the meaning their `--help` already gave them, and nothing here picks a winner between them — the overlap was in the emitter, never in the two meanings.
13+
14+
`'full'` is renamed `'stack'` and omits `metadataForms`, so the module a run writes and the baseline companion beside it are disjoint, and under `'stack'` the two together are everything the extractor built (3 + 773 = 776 on the fixture above — the extractor's own count, none dropped, none duplicated). ⚠️ That is a statement about the PAIR a run emits, not about "three kinds partitioning the leaves": `'objects'` is a sub-selection of `'stack'`, not a sibling of it.
15+
16+
`--json`, documented as "output JSON instead of writing files", mirrors that file set: `bundles` is the stack module and a `metadataForms` map is the companion, keyed by the locales whose companion would be written and gated by the same predicate. That map is new. It exists because the first cut of this change stopped the fold on the `--json` face as well and left the baseline with no JSON home at all — measured, `--json --no-objects-only` with the flag ON and with `--no-metadata-forms` returned payloads equal in every field but `duration`, so on that face the flag decided nothing, the mirror image of the defect this card reports. `metadataFormsCounts` reports the baseline's size in every run, as before.
17+
18+
**No bundle in this repository moves.** All nine extract configs run under the default `--objects-only`, whose emitted module, export name and type signature are byte-for-byte unchanged — `pnpm check:i18n` stays green on the committed tree. A stack that DOES pass `--no-objects-only` regenerates a smaller `<locale>.objects.generated.ts`: its export keeps its name and narrows from `TranslationData` to `Omit<TranslationData, 'metadataForms'>`, and the baseline it used to duplicate is in the companion beside it unless `--no-metadata-forms` says it should not be there at all.
19+
20+
**What content moves where.** On the file face nothing published loses content: under the default `--objects-only` the output is byte-identical, and under `--no-objects-only` the baseline moves out of the stack module into the companion the same command already writes — unless `--no-metadata-forms` says it should not exist, which is the ask. On the `--json` face the baseline moves from inside `bundles` to its own top-level key, and under `--no-metadata-forms` it is now absent, which it never was before: that face did not honour the flag at all.
21+
22+
The regression pin spawns the real CLI and takes a group census of the bytes it wrote, and drives `--json` in BOTH flag states. The one-state version of that case could not have failed on the axis that failed here — a pin that exercises only the flag-OFF path can never detect a flag that does nothing. The sibling pin that mirrors the emit rule and checks file NAMES stayed green through all of this: the file set was right in every combination, and only the content was wrong.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
The package-publish door's route-level seed apply can consume the platform's own read-back envelope again.
6+
7+
`POST /packages/:id/publish-drafts` reads each just-published `seed` body back through `protocol.getMetaItem` before handing it to the seed loader. That read exits through `decorateMetadataItem`, which stamps `_diagnostics` on every body whose metadata type has a registered schema — `seed` has one — and `SeedSchema` has been closed since protocol 17. So the door refused the document it had just been served: `unrecognized_keys: ["_diagnostics"]`, minted as a 422 and delivered on a **200** as `seedApplied.error`. Zero rows loaded, and the author was told their seed body failed spec validation when nothing about it was wrong.
8+
9+
The read-back is now passed through `stripReadDecorations` at the unwrap — the same helper, for the same reason, that the dataset query, the cold-boot flow bind and `saveMetaItem`'s verbatim persist already call. `METADATA_READ_DECORATIONS` is the declared list of keys the read path derives from a document and attaches to the *response*, so removing them restores the document the author actually wrote.
10+
11+
Nothing is widened to accept them: `SeedLoaderRequestSchema` stays closed, and the publish response keeps its declared shape. The strip is deliberately **not** a blanket `startsWith('_')` sweep — the ADR-0010 protection envelope (`_packageId`, `_provenance`, …) is not a read decoration, and the metadata schemas allowlist it precisely so a served document keeps its provenance when it is parsed again.
12+
13+
Only protocols that do not self-apply seeds inside `publishPackageDrafts` reach this path; the shipping protocol self-applies and was never affected.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 席位章程:分诊席(Triage)
2+
3+
见 SKILL.md 〈分诊座位职责〉;本文是本席的岗位说明,现值状态恒在座位贴,⛔ 不迁入本文。
4+
5+
- 分诊每 fire 只取一个仓,两仓轮替:objectstack、objectui、objectstack……;仓内按最旧优先。
6+
- 饥饿守卫:轮次开始时任一仓最旧未路由卡超过 4 小时,该仓本轮优先,不论轮到谁。
7+
- ⛔ 不回退到跨仓全局最旧优先;⛔ 不因积压增设第二分诊席或改 fire 频率。

content/docs/permissions/system-context.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ The largest single consumer — **17 of the 105 sites**.
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
161161
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5319`, `:6766`, `:7014`, `:7445`, `:7638` |
162162
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
163-
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:543`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
163+
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:552`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
165165
| 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `package-routes.ts:102` |
166-
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:241`, `:274` |
166+
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:250`, `:283` |
167167
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `activation-gate.ts:138`, `:189` |
168168
| 57 | Automation run-state read, flow-authoring write and unrelated-screen read all pass | runtime | Get: run state, flow writes and screen reads with no grant | `domains/automation.ts:254`, `:545`, `:635` |
169169
| 58 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `suggested-audience-bindings.ts:703` |

packages/cli/src/commands/i18n/extract.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
extractTranslations,
2222
renderTranslationModule,
2323
renderSourceHashModule,
24+
stackAuthoredSubtree,
2425
parseSourceHashModule,
2526
narrowToCommittedSections,
2627
type FillStrategy,
@@ -87,13 +88,14 @@ export default class I18nExtract extends Command {
8788
default: false,
8889
}),
8990
'objects-only': Flags.boolean({
90-
description: 'Emit only the objects/globalActions subtree (default). Disable to include apps/dashboards.',
91+
description:
92+
'Emit only the objects/globalActions subtree (default). Disable to include apps/dashboards. Never carries the Studio metadata-form baseline either way — that is --metadata-forms, which writes it to its own file.',
9193
default: true,
9294
allowNo: true,
9395
}),
9496
'metadata-forms': Flags.boolean({
9597
description:
96-
'Also write <locale>.metadata-forms.generated.ts for the Studio metadata-form baseline (default). Pass --no-metadata-forms in a package that owns only its own objects — that baseline belongs to one package, not every plugin.',
98+
'Also write <locale>.metadata-forms.generated.ts for the Studio metadata-form baseline (default). Pass --no-metadata-forms in a package that owns only its own objects — that baseline belongs to one package, not every plugin. This is the only control over it: no other flag emits or suppresses that baseline.',
9799
default: true,
98100
allowNo: true,
99101
}),
@@ -197,6 +199,16 @@ export default class I18nExtract extends Command {
197199
// only its own objects passes `--no-metadata-forms`; without it, `--check`
198200
// demands a baseline copy the package deliberately does not commit and
199201
// fails on a tree that is in fact in sync.
202+
//
203+
// ⚠️ That orthogonality was a claim this file made and did not keep
204+
// (#14894). It held only while `--objects-only` was in effect: under
205+
// `--no-objects-only` the renderer's `kind: 'full'` folded the baseline
206+
// into the objects module, so `--no-metadata-forms` suppressed a copy
207+
// that was still being written next door — and with the flag left on,
208+
// both copies were written. This predicate is now the ONLY thing that
209+
// decides whether the baseline is emitted, because the stack module no
210+
// longer carries it (`stackAuthoredSubtree`). Nothing here picks a winner
211+
// between the two flags; there is no longer anything for them to contest.
200212
const emitsMetadataForms = (locale: string): boolean =>
201213
flags['metadata-forms'] && (metadataFormsCounts[locale] ?? 0) > 0;
202214

@@ -251,9 +263,42 @@ export default class I18nExtract extends Command {
251263
totalExpected: result.totalExpected,
252264
counts: result.counts,
253265
metadataFormsCounts,
254-
bundles: objectsOnly
255-
? Object.fromEntries(localesEmitted.map((l) => [l, result.bundles[l].objects ?? {}]))
256-
: result.bundles,
266+
// `--json` is documented as "output JSON instead of writing files",
267+
// so this payload mirrors the FILE SET: `bundles` is the stack
268+
// module, `metadataForms` below is the companion (#14894).
269+
bundles: Object.fromEntries(
270+
localesEmitted.map((l) => [
271+
l,
272+
objectsOnly ? (result.bundles[l].objects ?? {}) : stackAuthoredSubtree(result.bundles[l]),
273+
]),
274+
),
275+
// The baseline's JSON home, gated by {@link emitsMetadataForms} —
276+
// the SAME predicate that decides the companion file, deliberately
277+
// not a second one.
278+
//
279+
// ⚠️ Two predicates is what the review of this card's first commit
280+
// caught, and the reading is worth keeping: that commit stopped the
281+
// `kind: 'full'` fold on this face too, and left the baseline with no
282+
// JSON home at all. Driven on a one-object, one-app stack with
283+
// `defaultLocale: 'zh-CN'`, `--json --no-objects-only` with the flag
284+
// ON and with `--no-metadata-forms` produced payloads that were equal
285+
// in every field but `duration` — 3 leaves in `bundles`, no baseline
286+
// in either, and `metadataFormsCounts` reporting 773 in both. So on
287+
// this face the flag decided NOTHING, in the opposite direction from
288+
// the defect the card reported (where it was the fold that ignored
289+
// it). A flag that is ignored is a flag that is ignored, whichever
290+
// way the output falls.
291+
//
292+
// Keyed by locale and PRESENT ONLY for the locales whose companion is
293+
// written, so the key set here and the `*.metadata-forms.generated.ts`
294+
// set are the same set by construction. The map itself is always
295+
// emitted — an empty map says "no baseline in this run", which is a
296+
// reading; a missing key would be indistinguishable from an older CLI.
297+
metadataForms: Object.fromEntries(
298+
localesEmitted
299+
.filter((l) => emitsMetadataForms(l))
300+
.map((l) => [l, result.bundles[l].metadataForms ?? {}]),
301+
),
257302
duration: timer.elapsed(),
258303
});
259304
return;

0 commit comments

Comments
 (0)