Skip to content

Commit 290a435

Browse files
committed
Merge origin/main into claude/project-thread-g33c7k
Brings in main's re-measured authz census (#26406), the fix for main's own dogfood red (#26400) that the full battery on this branch showed. Main's automation skill change (#26390) left the approvals reference two bytes under its token ceiling, so the queue approver row is shortened to fit (members of the named ownership queue at node entry; an empty queue adds nobody). Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
2 parents aea6b12 + 9a0108c commit 290a435

49 files changed

Lines changed: 2050 additions & 329 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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — the two unread dependency spellings leave the authoring surface: `PackageDependency.optional` (an entry of the marketplace snapshot's `manifestJson.dependencies[]`) and `AppManifest.dependencies` (the bare-id list on the app install manifest) are `retiredKey()` tombstones (#25792, ADR-0141 D8 items 2 and 3).
6+
7+
A `major`-class change, recorded as `minor` under the launch-window convention. ADR-0141 D8 left both fates to "a measured reader count"; the count was zero for both, so both retire at once, with no retirement stage (ADR-0049 enforce-or-remove).
8+
9+
Clause-②: no (narrowing)
10+
Metadata-protocol: narrowed
11+
12+
**Why.** A dependency is declared in one place, the package's `manifest.dependencies` record (`{ [packageId]: versionRange }`), and the ADR-0141 D2 pre-install check reads that record alone and treats every entry as required. Two more spellings stayed declared beside it and nothing read either. `optional: true` promised "missing dep is a warning, not an error", but the record has no optional arm, so no install path could honour it. `AppManifest.dependencies` restated the record without its range, and no install path consulted it. An author, often an AI, who wrote either got nothing enforced and no error. Measured at `origin/main` `6390fe52ce`: zero readers and zero writers of either key outside `packages/spec` (`packages/`, `ui/`, `examples/`, `apps/`), and none in HotCRM at `9464673`, whose three modules declare their dependency in the record.
13+
14+
### FROM → TO
15+
16+
| removed | from | what to write instead |
17+
| --- | --- | --- |
18+
| `optional` on a `PackageDependency` entry (`PackageManifestSchema.dependencies[]`) | `@objectstack/spec/marketplace` | delete the key. An entry carries `packageId` and `versionRange` only. Declare a dependency in `manifest.dependencies` only when the package cannot install without it. |
19+
| `dependencies` on `AppManifest` | `@objectstack/spec/system` | delete the key. Declare each dependency with its range in the package's `manifest.dependencies`, for example `{ 'com.acme.core': '^1.0.0' }`. |
20+
21+
**The one-line fix: delete the key.** A TypeScript author fails `tsc` first (each key is typed as the retired mark), and a parse of `PackageDependencySchema`, `PackageManifestSchema` or `AppManifestSchema` refuses the key with its prescription, the old default (`false`, `[]`) included. The parsed output no longer carries either key, so `PackageDependencyParsed` loses `optional` and `AppManifestParsed` loses `dependencies`.
22+
23+
⚠️ Runtime behaviour is deliberately **unchanged**: nothing ever read either key, so every install answers exactly as before. The removal retracts a false claim, not a capability. **No deprecation window** (startup-stage posture: retirements take effect immediately).
24+
25+
⚠️ **The out-of-repo consumer population beyond HotCRM is NOT MEASURED.** `@objectstack/spec` is published, and the control plane's reading of `sys_package_version.manifest_json` is not readable from this repository.
26+
27+
The ADR-0087 D3 semantic entry `dependency-unread-spellings-retired` carries the judgement, and both keys are registered in `RETIRED_KEYS_BY_MAJOR[18]`. There is no D2 conversion: neither shape is a stack collection member, so a conversion would have no source to rewrite.
28+
29+
<!-- adr-0087: registered dependency-unread-spellings-retired -->
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/fields': patch
3+
'@object-ui/plugin-grid': patch
4+
---
5+
6+
fix(fields,plugin-grid): a grid's `date` column reads in one absolute face when its field declares no `format`
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
A list grid's `date` column used to mix three faces in one column, for example "Yesterday · 3 days ago · Sep 25 · Sep 20, 2025". The cell's default face was `'relative'`, which writes a relative phrase within ±7 days of today and an absolute date outside that window. A sorted column did not look sorted, and "Yesterday" in an expiry column read as a status.
12+
13+
- **Default change for an undeclared `format` (visible from metadata).** In a grid column, a `date` field that declares no `format` now renders the locale's short absolute date: `Oct 25`, with the year added when the date is not in the current year. Every row of the column uses this one face. The group headings of a grid grouped by that field change with it, so a heading still reads like the cell under it.
14+
- **The relative phrase moves to the tooltip.** It is shown on hover ("Yesterday", "In 2 days", or "Overdue 3d" on a declared `dueLike` field). A date outside the ±7-day window has no relative phrase, so it keeps its ISO `title` as before. A past declared deadline is still red.
15+
- **A declared `format` still wins.** A field that declares `format: 'relative'` keeps the relative face in the grid. This is how an author who wants relative wording in a list keeps it.
16+
- **Other surfaces are unchanged.** The record detail body, the header highlights strip, kanban and gallery cards, the tree, a record page's related list and the grid's record panel render `date` cells as before. `DateCellRenderer` paints the absolute default only when its host passes the new `inGridColumn` prop, and `ObjectGrid` passes it on every column it builds. On mobile, a grid card draws its title through the column's cell, so a `date` title reads absolute there too; the card's other date slots were already absolute. `datetime` cells are not affected, because their default face was already absolute (`compact`).
17+
18+
What to do: nothing. To keep the relative wording in a grid column, declare `format: 'relative'` on the field.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): a record locked by an approval is shown locked to a reader outside that approval
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A member who can read a record but is not on its approval (not an approver, not the submitter, no override) was offered inline edit while a `lockRecord: true` approval held the record, and the save was then refused with `409 RECORD_LOCKED`. The approvals list serves a request only to its participants, so the record page read that member's empty list as "no approval is running".
11+
12+
When the list finds no pending request, the record page now also reads the record's own lock, `client.approvals.getRecordLock(object, recordId)`, which the server answers to every caller who can read the record with `{ locked }` and nothing else. A locked answer shows the record locked: Edit is disabled, inline edit is not offered, and the "Locked for approval" band is drawn without a recall button, because a reader who cannot see the request did not submit it. The reader still sees nothing of the request itself (approvers, comments, attachments).
13+
14+
Unchanged: a pending request the viewer can see still decides with its own node's `lockRecord`, and a composition without the approvals service (the lock read answers `501`) still opens an answered empty record editable.

‎.github/workflows/lint.yml‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -631,15 +631,14 @@ jobs:
631631
# and it is the only instrument on this gate's matching rules -- a clean
632632
# tree cannot tell a working rule from a weakened one.
633633
#
634-
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
635-
# GATE INVOCATION IDIOM note at the top of this file.
634+
# Invoked through its `pnpm check:*` alias, so root `package.json` is the
635+
# one definition of the command (#26086).
636636
- name: Docs anchors resolve to real headings
637637
id: docs-anchors-resolve-to-real-headings
638638
continue-on-error: true
639639
run: |
640640
pnpm check:doc-anchors
641-
node scripts/check-system-context-census.mjs --self-test
642-
node scripts/check-system-context-census.mjs
641+
pnpm check:system-context-census
643642
644643
# #3723 ADR anchors: code an accepted ADR governs must keep naming it.
645644
# That incident reversed three accepted ADRs with a patch-level changeset,
@@ -908,24 +907,20 @@ jobs:
908907
# tell a working matcher from a dead one, and the specifier FLOOR is the
909908
# other half (a dead matcher finds nothing, and nothing is what success
910909
# looks like — so it refuses with exit 2 rather than passing).
911-
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
912-
# GATE INVOCATION IDIOM note at the top of this file.
910+
# Invoked through its `pnpm check:*` alias, so root `package.json` is the
911+
# one definition of the command (#26086).
913912
# Scans ~2k non-test src files across 78 workspace packages, no spawns; ~1s.
914913
- name: Published src imports only declared workspace deps
915914
id: published-src-imports-only-declared
916915
continue-on-error: true
917-
run: |
918-
node scripts/check-undeclared-dep-imports.mjs --self-test
919-
node scripts/check-undeclared-dep-imports.mjs
916+
run: pnpm check:undeclared-dep-imports
920917

921918
# Rationale: `scripts/check-keyed-text-bounds.mjs` header (#12147, #11374, #12058,
922919
# #12143).
923920
- name: Keyed text-family columns declare their bound (#12147)
924921
id: keyed-text-family-columns-declare
925922
continue-on-error: true
926-
run: |
927-
node scripts/check-keyed-text-bounds.mjs --self-test
928-
node scripts/check-keyed-text-bounds.mjs
923+
run: pnpm check:keyed-text-bounds
929924

930925
# Rationale: `scripts/check-bash32-floor.mjs` header (#12221).
931926
- name: shell scripts hold the bash 3.2 floor
@@ -1968,9 +1963,7 @@ jobs:
19681963
- name: Platform-object tenancy census matches the tree
19691964
id: platform-object-tenancy-census-matches
19701965
continue-on-error: true
1971-
run: |
1972-
node scripts/check-platform-object-tenancy-census.mjs --self-test
1973-
node scripts/check-platform-object-tenancy-census.mjs
1966+
run: pnpm check:platform-object-tenancy-census
19741967

19751968
# ── THE LANE'S VERDICT (#23191): the last step, and the one that keeps a
19761969
# red gate red. The EVERY GATE RUNS note above `jobs:` argues it. ─────
@@ -3593,7 +3586,7 @@ jobs:
35933586
- name: Declared registry log level
35943587
id: declared-registry-log-level
35953588
continue-on-error: true
3596-
run: node scripts/check-registry-log-declared.mjs --self-test && node scripts/check-registry-log-declared.mjs
3589+
run: pnpm check:registry-log-declared
35973590

35983591
# The same defect one package down (#15484). `packages/rest`'s own fault
35993592
# logging hands whole `Error` objects to `console.error`, and Node prints
@@ -3608,7 +3601,7 @@ jobs:
36083601
- name: Declared REST fault-log level
36093602
id: declared-rest-fault-log-level
36103603
continue-on-error: true
3611-
run: node scripts/check-rest-log-declared.mjs --self-test && node scripts/check-rest-log-declared.mjs
3604+
run: pnpm check:rest-log-declared
36123605

36133606
# The pairing half of the one above (#17865, ruling batch #128 item 4).
36143607
# The `packages/rest` harness now runs at `OS_REST_LOG: 'silent'`, which
@@ -3623,7 +3616,7 @@ jobs:
36233616
- name: Declared REST fault-log level — observers
36243617
id: declared-rest-fault-log-level-observers
36253618
continue-on-error: true
3626-
run: node scripts/check-rest-log-spy-declared.mjs --self-test && node scripts/check-rest-log-spy-declared.mjs
3619+
run: pnpm check:rest-log-spy-declared
36273620

36283621
# Rationale: `scripts/check-live-db-isolation.mjs` header (#10382, #9350).
36293622
- name: Live-server database isolation

‎content/docs/references/marketplace/package-version.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Package dependency declaration
6464
| :--- | :--- | :--- | :--- |
6565
| **packageId** | `string` | ✅ | Manifest ID of the dependency |
6666
| **versionRange** | `string` | ✅ | Semver version range (e.g. ^1.0.0) |
67-
| **optional** | `boolean` | optional (default: `false`) | Whether this dependency is optional |
67+
| **optional** | `never` | optional | [REMOVED] `PackageDependency.optional` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0141 D8) — no install path ever honoured it: the pre-install check reads the package's `manifest.dependencies` record, which has no optional arm, and treats every entry as required. Delete the key. Declare a dependency in `manifest.dependencies` (`{ 'com.acme.core': '^1.0.0' }`) only when the package cannot install without it. |
6868

6969

7070
---
@@ -83,7 +83,7 @@ Package manifest snapshot embedded in a package version
8383
| **description** | `string` | optional | Short description |
8484
| **scope** | `Enum<'platform' \| 'environment'>` | optional (default: `"environment"`) | Package scope |
8585
| **minPlatformVersion** | `string` | optional | Minimum required platform version (semver) |
86-
| **dependencies** | `{ packageId: string; versionRange: string; optional?: boolean }[]` | optional (default: `[]`) | Package dependencies |
86+
| **dependencies** | `{ packageId: string; versionRange: string }[]` | optional (default: `[]`) | Package dependencies |
8787
| **metadataTypes** | `string[]` | optional (default: `[]`) | Metadata types provided by this package |
8888
| **migrations** | `string[]` | optional (default: `[]`) | Migration script identifiers (ordered) |
8989
| **configurationSchema** | `Record<string, any>` | optional | JSON Schema for per-installation configuration properties |
@@ -97,7 +97,7 @@ Package dependency declaration
9797
| :--- | :--- | :--- | :--- |
9898
| **packageId** | `string` | ✅ | Manifest ID of the dependency |
9999
| **versionRange** | `string` | ✅ | Semver version range (e.g. ^1.0.0) |
100-
| **optional** | `boolean` | optional (default: `false`) | Whether this dependency is optional |
100+
| **optional** | `never` | optional | [REMOVED] `PackageDependency.optional` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0141 D8) — no install path ever honoured it: the pre-install check reads the package's `manifest.dependencies` record, which has no optional arm, and treats every entry as required. Delete the key. Declare a dependency in `manifest.dependencies` (`{ 'com.acme.core': '^1.0.0' }`) only when the package cannot install without it. |
101101

102102

103103
---

‎content/docs/references/system/app-install.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ App manifest for marketplace installation
111111
| **flows** | `string[]` | optional (default: `[]`) | Flow names provided |
112112
| **hasSeedData** | `boolean` | optional (default: `false`) | Whether app includes seed data |
113113
| **seedData** | `Record<string, any>[]` | optional (default: `[]`) | Seed data records |
114-
| **dependencies** | `string[]` | optional (default: `[]`) | Required app dependencies |
114+
| **dependencies** | `never` | optional | [REMOVED] `AppManifest.dependencies` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0141 D8) — nothing read it: it listed bare ids with no version range, and no install path consulted it. Delete the key. Declare each dependency with its range in the package's `manifest.dependencies` (`{ 'com.acme.core': '^1.0.0' }`), which the pre-install check enforces. |
115115

116116

117117
---

‎docs/adr/0141-inter-app-dependency-declared-checked-before-install.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,17 @@ other two on the runtime card:
513513
The resolver-side `ResolvedPackageDependencySchema` (`@objectstack/spec/kernel`) is unaffected: it is a resolution outcome,
514514
not a declaration, and `PackageDependencySchema`'s own header already tells the two apart.
515515

516+
**Addendum (2026-10-27, #25792 item 6) — items 2 and 3 decided on the measured count: both retire.** Measured at
517+
`origin/main` `6390fe52ce`: `PackageDependencySchema.optional` has **0** readers and **0** writers outside `packages/spec`,
518+
and `AppManifestSchema.dependencies` has **0** readers and **0** writers there, with no parse of `AppManifestSchema` at all
519+
(this repository's `packages/`, `ui/`, `examples/` and `apps/`; HotCRM at `9464673`, whose three module manifests declare
520+
the record, the live control). Inside `packages/spec` the only authors were the two schemas and three of their own tests.
521+
Each key is a `retiredKey()` tombstone now (`marketplace/PackageDependency:optional` and `system/AppManifest:dependencies`
522+
in `RETIRED_KEYS_BY_MAJOR[18]`), with the D3 record `dependency-unread-spellings-retired` and no D2 conversion, because
523+
neither shape is a stack collection member. The projection above therefore carries `packageId` = the key and
524+
`versionRange` = the value, nothing else: item 1's conversion writes no `optional`, and `manifest.dependencies` is the
525+
one way to declare a dependency.
526+
516527
---
517528

518529
## Consequences
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 21] The boot report of `ObjectStackProtocolImplementation`
5+
* that says out loud which stored ENVIRONMENT `sys_metadata` rows no read
6+
* serves because they overlay sealed managed content
7+
* (`reportSealedOverlayRows`, ADR-0131 D6): per type, a capped sample of
8+
* names and the two remedies. Moved out of `protocol.ts` whole.
9+
*
10+
* A MOVE, not a redesign. The function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same grouping by type,
13+
* the same sample cap, and the same warning text.
14+
*
15+
* ## How the moved body reaches the class
16+
*
17+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
18+
* header carries the full reasoning). The class keeps the method — the
19+
* published `dist/*.d.ts` lists its name, docblock and declared signature.
20+
* The body reads no member of the class, so there is no host: the method's
21+
* stub calls the plain function directly, as `namesWithOwnStoredRow`'s does
22+
* in `protocol-stored-overlay-reads.ts`. Its one caller keeps calling the
23+
* class method.
24+
*
25+
* The body reads none of `protocol.ts`'s module-level declarations and
26+
* imports nothing. Nothing re-exports this module, so the published surface
27+
* does not move.
28+
*/
29+
30+
/** The body of `ObjectStackProtocolImplementation#reportSealedOverlayRows`; the method's docblock there is its contract. */
31+
export function reportSealedOverlayRows(rows: ReadonlyArray<{ type: string; name: string }>): void {
32+
if (rows.length === 0) return;
33+
const SAMPLE_PER_TYPE = 5;
34+
const byType = new Map<string, string[]>();
35+
for (const { type, name } of rows) {
36+
const names = byType.get(type) ?? [];
37+
names.push(name);
38+
byType.set(type, names);
39+
}
40+
const detail = Array.from(byType.entries())
41+
.map(([type, names]) => {
42+
const shown = names.slice(0, SAMPLE_PER_TYPE);
43+
const more = names.length > shown.length ? `, +${names.length - shown.length} more` : '';
44+
return `${type}×${names.length} (${shown.join(', ')}${more})`;
45+
})
46+
.join('; ');
47+
console.warn(
48+
`[Protocol] [metadata_sealed_overlay_unserved] ${rows.length} active environment sys_metadata row(s) ` +
49+
`overlay an item a managed package ships, on a type that is sealed against overlays (ADR-0131 D6): ` +
50+
`${detail}. No read serves them — the package's own definition is served. ` +
51+
`Each row is kept at rest, untouched (an object's table and its columns stay as they are). To keep ` +
52+
`the change, re-express it as a new item under a new ` +
53+
`name (a linkage-free clone of the managed item), then delete the stored row; otherwise delete the ` +
54+
`stored row.`,
55+
);
56+
}

0 commit comments

Comments
 (0)