Skip to content

Commit 6dceb9a

Browse files
committed
Merge origin/main into claude/issue-19457-charter-product-only-queue
Resolves the one conflict, in `.claude/skills/pm-dispatch/SKILL.md`, keeping both sides. #19459 rewrote the grading line this card also edits; main's four-segment `Path:` spelling and its two new lines (定义项 / 清单项) are kept verbatim, and this card's three additions sit beside them: - the grading reference now reads North Star 「优先级」1–3 条 (main had 1、2); 「1–3」 rather than 「1、2、3」 because the latter spelling puts the line at 121 bytes against the ratchet's 120-byte cap. - the close-at-first-touch rule is folded onto main's own 「无则关」 clause instead of restating it: it carries only what main lacks — not_planned, the reason, the two reopen conditions, and ⛔ 不定 p3、不 hold. - the tooling closing rule (North Star rule 3) follows it and inherits that prohibition through 「下行同此」. Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE Co-authored-by: Claude <noreply@anthropic.com>
2 parents f2977f1 + fbc12be commit 6dceb9a

149 files changed

Lines changed: 3917 additions & 675 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Two published `describe` sentences that the `.objectui-sha` bump to objectui `87af769e9a3e` makes false are corrected against the new pin (#17429).
6+
7+
`Clause-②: no`
8+
9+
Both are claims about what the SHIPPED console renderer does, so the pin is what dates them — and both were re-read by executing the pinned tree, not by refreshing a sha.
10+
11+
- `Dataset` measure `format`: the clause said "a datetime value ignores it". objectui#8352 lands inside the `53ded82bf7a4...87af769e9a3e` range and makes the datetime arm of `formatMeasureDate` honour the same two words the date arm does — `relative` through `formatRelativeDate`, `short` through `formatDateTime(v, { locale, style: 'compact' })`. A date PATTERN is still ignored on both arms, which is the half of the sentence that survives.
12+
- `FormField.span`: the clause said only the widest container-query tier's class is emitted, so a `'full'` field took one cell of two at the 720px modal width (objectstack#17328). objectui#9244 / objectui#9253 (objectui `bd09957380`) are also inside the range: `spanLadderFor` now emits one clamped col-span class per multi-column tier, so `'full'` is the whole row at every multi-column tier. The `'auto'` half — textarea, markdown, html, richtext and repeater — re-measured unchanged at the new pin.
13+
14+
No key, default, enum member or export moves: the same authored metadata is accepted and refused as before, and `content/docs/references/ui/{dataset,view}.mdx` are regenerated from these two sentences.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
"create-objectstack": minor
5+
---
6+
7+
feat(spec)!: `manifest.id` enforces the reverse-domain rule its registry face already had (#17534)
8+
9+
<!-- adr-0087: registered manifest-id-reverse-domain-required -->
10+
11+
**BREAKING** in the accept-set sense, landing in the launch window as `minor`
12+
(the repo's convention: `major` is refused by `check-changeset-no-major`, and
13+
breaking-ness is carried by this banner plus the ADR-0087 disposition):
14+
`ManifestSchema.id` was `z.string()` and accepted any string. It now enforces
15+
reverse-domain notation — the same rule `PackageSchema.manifestId` has always
16+
carried, now declared once and referenced from both sites so the two cannot
17+
drift again.
18+
19+
Two declarations named one identity and disagreed. The registry enforced the
20+
shape; the key an author actually writes did not. So a package scaffolded,
21+
validated, built and booted with an id the publish path would refuse, and the
22+
author met the rule for the first time at the most expensive possible moment.
23+
24+
FROM → TO, for metadata that used to parse and now fails:
25+
26+
```ts
27+
// FROM — accepted by defineStack, refused at publish
28+
defineStack({ manifest: { id: 'my_app', /* … */ } });
29+
defineStack({ manifest: { id: 'com.acme.my_app', /* … */ } });
30+
31+
// TO — dot-separated lowercase segments; hyphens inside a segment, never underscores
32+
defineStack({ manifest: { id: 'com.example.my-app', /* … */ } });
33+
defineStack({ manifest: { id: 'com.acme.my-app', /* … */ } });
34+
```
35+
36+
The refusal carries the repair rather than restating the rule: it names the key,
37+
echoes the value, shows both documented examples, and — having first checked the
38+
candidate against the pattern itself — suggests `com.example.blank` for a bare
39+
word and `com.dogfood.flow-fixture` for a value whose only fault is an
40+
underscore. A suggestion it cannot verify it does not make.
41+
42+
⚠️ **Changing an id is a republish, not an edit.** An id is an identity: the
43+
registry addresses a package by `manifest_id`, an installed row is keyed on it
44+
and a dependent declares it. Before renaming, confirm nothing still addresses
45+
the old value. That is why this ships as an ADR-0087 **semantic** entry
46+
(`manifest-id-reverse-domain-required`) with a structured TODO and no automatic
47+
rewrite — `objectstack migrate meta` will not rename an id for you.
48+
49+
`manifest.namespace` is unchanged and still admits underscores, so the two are
50+
derived from a project name under different rules and neither is the other. Both
51+
scaffolders were producing ids the new rule refuses and both now derive a
52+
conforming one: the bundled `create-objectstack` template ships
53+
`com.example.blank` and interpolates `com.example.<project-name>` in kebab form,
54+
and `os init` derives its id from the project name instead of interpolating the
55+
snake_case namespace (`os init my-app` produced `com.example.my_app`).
56+
57+
## ⚠️ One consent path reverses direction: fail-OPEN → fail-CLOSED
58+
59+
Narrowing `manifest.id` also narrows the **accept set of the artifact load
60+
path**, and on one route that is a **fail-OPEN → fail-CLOSED reversal on a
61+
consent/permission path**. Stating it explicitly because a reversal in that
62+
direction is owed a named direction and a named population, however small the
63+
population turns out to be.
64+
65+
**What changed.** `AssembledPackageBodySchema` extends `ManifestSchema`, so the
66+
artifact package entry schema now carries this rule too. An assembled package
67+
whose `manifest.id` is `''` used to parse: `artifactPackageId` is
68+
`manifest.id || manifest.name`, so such a package was carried under its `name`,
69+
while an install-time `grantedPermissions` record keyed by `''` matched no
70+
carried package and was registered nowhere. The package loaded **with no
71+
consent record at all** — reported as unbound, warned about, and otherwise
72+
allowed to run. That is the fail-OPEN half. Such an entry is now refused
73+
outright (`INVALID_ARTIFACT_PACKAGE_ENTRY`, 422) and the artifact does not
74+
materialize at all — fail-CLOSED.
75+
76+
**Who is affected: artifacts carrying `manifest.id: ''`, and they were already
77+
half-broken in both directions.**
78+
79+
- They could never be **published**: the registry face
80+
(`PackageSchema.manifestId`) has carried this exact pattern all along — the
81+
same regex literal, now the shared `MANIFEST_ID_PATTERN` — so the publish path
82+
has always refused them.
83+
- Their granted-permissions **consent already did not apply**: a record keyed by
84+
`''` bound to nothing, silently, on every load.
85+
86+
⇒ For that population this converts a silent, already-ineffective consent
87+
binding into an explicit refusal that names `manifest.id`. Nobody who could
88+
publish an artifact loses the ability to load it; what they lose is a shape that
89+
only ever half-worked.
90+
91+
⛔ This is the **artifact package door** refusing a malformed id, **not** the
92+
permission enforcer acquiring teeth. The install-time granted permission set is
93+
still registered and not enforced (#17147) — nothing on the tree queries that
94+
registry, and the repo-wide pin asserting so is unchanged and still green.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/sdui-parser": minor
3+
---
4+
5+
The save gate now stamps `inert-quick-add` and `member-type-mismatch`, the two diagnostics that existed only in objectui's copy of this parser — so a page no longer saves clean here and renders with a different verdict there (#17645).
6+
7+
The two copies of this parser owe each other one thing: byte agreement on the accepted grammar and on diagnostic codes. This copy runs the **save gate** and objectui's runs the **renderer**, so a code on one side only is a dialect — the author gets one reading when they save and another when the page draws, which is surface-dependent and therefore reaches them as intermittent. Measured at the ported revision: objectui stamped 26 codes, this copy stamped 24, and the missing two were exactly these.
8+
9+
- **`inert-quick-add`** (warning) — `quickAdd` on `<object-kanban>` reaches no control. The Quick Add button is gated on **both** `quickAdd` and an `onQuickAdd` handler, and `onQuickAdd` takes a function, which no page on this tier can write (this tier parses, it never executes) and which the board substitutes none of its own for. It **replaces** the `unknown-prop` this copy used to emit for the key, which was false against the contract: `ComponentPropsMap['object-kanban']` publishes `quickAdd`, so an author who checked the spec found the warning contradicted and kept a key that will never do anything. Asked ahead of the declaration lookup on purpose — the claim is about the render path, so declaring the key must not silently disarm it. A falsy value and an unevaluated braced expression are deliberately untouched.
10+
- **`member-type-mismatch`** (warning; `error` when an `enum` arm is present) — the coarse type check one level down, over the member kind an input declares. This brings the `ManifestInput.of` key and its three readers with it: the validator, the serializer's canonicalization, and the codegen's element type. `of: 'string'` on an array input now types the members `string[]` in the generated `.d.ts` instead of `unknown[]`, and a member no declared arm accepts draws **one** diagnostic naming every offending position rather than one per member.
11+
12+
**Nothing published changes shape for an input that declares no `of`.** The key is absent-means-undeclared: the validator checks no member, the codegen emits the unnarrowed element type, and `manifestFromConfigs` publishes no `of` at all, so an entry written before the key existed serializes byte-identically. Measured on the tracked `sdui.manifest.json`: 0 of 339 inputs declare `of`, and the artefact regenerates to the same sha256 across this change.
13+
14+
⚠️ **Both new codes are diagnostics, not a new red gate.** Each is a warning, so `compile().ok` — the save gate's pass/fail — is unchanged, and a page that saves today still saves. Escalating an inert authored key to `error` is a separate question and belongs at the save gate, not here.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
'@objectstack/spec': minor
4+
---
5+
6+
fix(plugin-security): the `sys_permission_set` duplicate-name refusal carries `UNIQUE_VIOLATION`, and the packaged-set lock answers first (#19307)
7+
8+
Clause-②: yes
9+
10+
Two halves of one defect on the data door's insert leg for `sys_permission_set`
11+
(`permission-set-projection.ts`), both measured live on `examples/app-showcase`
12+
with a seeded admin over a cookie session.
13+
14+
**1. The refusal carried no machine-readable code.** It threw a bare `Error`
15+
with `.status = 409` and no `.code`, and the flat `{ error, code }` responder
16+
invents nothing for a producer that declared nothing, so the client got prose:
17+
18+
```
19+
POST /api/v1/data/sys_permission_set {"name":"dev_local_set"}
20+
→ 409 {"error":"[Security] permission set 'dev_local_set' already exists","object":"sys_permission_set"}
21+
```
22+
23+
ADR-0112's 2026-08-17 amendment closed `error.code` at the flat door too, so a
24+
409 with no code is that contract unhonoured — and a UI that has to branch on
25+
the refusal was pushed back to string-matching. The same request now answers
26+
`409 … "code":"UNIQUE_VIOLATION"`, message byte-identical.
27+
28+
⚠️ `UNIQUE_VIOLATION` is REUSED, not minted. `sys_permission_set` declares
29+
`{ fields: ['name'], unique: 'organization' }`, so this very collision already
30+
answers `409 UNIQUE_VIOLATION` when the index catches it instead of this
31+
pre-check; a second spelling would make one condition answer two envelopes
32+
depending only on which layer got there first. The ledger gains a provenance
33+
row for `@objectstack/plugin-security` — the union, its casing and every other
34+
package's rows are unchanged, and no schema shape moves.
35+
36+
**2. It ran BEFORE the packaged-set lock, so the most likely path answered the
37+
less useful of two true refusals.** A package-declared set has a projected row,
38+
so its name is duplicate AND locked at once. An admin who opened the Clone
39+
dialog on a packaged set and typed the base set's own name — the single most
40+
likely thing to type — got `already exists`, which names no remedy, and never
41+
reached `NOT_OVERRIDABLE`, which names the clone path. The lock now runs first:
42+
43+
```
44+
POST /api/v1/data/sys_permission_set {"name":"showcase_manager"}
45+
→ 403 {"error":"[Security] Permission set 'showcase_manager' is declared by package
46+
'com.example.showcase' and is locked … Choose a different name for your set, or clone
47+
'showcase_manager' …","code":"NOT_OVERRIDABLE","object":"sys_permission_set"}
48+
```
49+
50+
**What did NOT move**, measured on the same runtime: an ordinary
51+
(non-package-declared) duplicate **whose provenance the lock can resolve** still
52+
answers the duplicate refusal and not `NOT_OVERRIDABLE` — that qualifier is
53+
load-bearing, and the corner below is the case it excludes; an unauthenticated
54+
write on the same resource still answers `401 UNAUTHENTICATED`; and an `update`
55+
targeting a packaged set answers `403 NOT_OVERRIDABLE` exactly as before.
56+
57+
⚠️ **One corner moved with the order**: an ordinary duplicate attempted while no
58+
artifact source can answer now takes the lock's fail-closed `unknown` refusal —
59+
`403` `NOT_OVERRIDABLE` (`PackagedPermissionSetProvenanceUnknownError`, "retry
60+
once the metadata layer is readable") — instead of the 409. Both are refusals and
61+
neither writes; it is pinned so the behaviour is declared rather than incidental.
62+
63+
⚠️ **And the order has a cost, stated rather than discovered**: the lock's probe
64+
(`protocol.getMetaItemLayered`) used to be evaluated only AFTER the duplicate
65+
check passed, so a duplicate insert never paid for it. It is now evaluated
66+
unconditionally, ahead of that check. Two consequences, both deliberate: every
67+
**duplicate** insert on `sys_permission_set` costs one extra metadata round trip
68+
(the accepted path's cost is unchanged — it always paid this probe), and the
69+
duplicate path is now COUPLED to metadata-layer reachability, where before it
70+
answered from the record alone. That coupling is the mechanism behind the corner
71+
above, and it is the price of putting the refusal that names the remedy first.

0 commit comments

Comments
 (0)