Skip to content

Commit 65a07bb

Browse files
committed
Merge origin/main into claude/issue-16348-composestacks-envelopes
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
2 parents 94d9672 + 97f4f8c commit 65a07bb

9 files changed

Lines changed: 520 additions & 462 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/plugin-sharing': patch
4+
---
5+
6+
`plugin-sharing` recognises the engine's organization refusal through objectql's own published recognizer instead of a locally re-spelled literal, and the `PROVENANCE_WAIVERS` row that excused that local spelling is retired with it (#16160).
7+
8+
Clause-②: no
9+
10+
The waiver carried its own expiry in its `reason`: *removed together with the stamp site when objectql publishes a recognizer*. It does, so both halves land here — `check:error-code-provenance` reconciles a waiver in three directions at once (the `registeredUnder` key still lists the code, the waived package still does not, and the scan still finds a site for the pair), so removing either half alone reddens the gate on the other.
11+
12+
- **`ENGINE_ORGANIZATION_REFUSAL_CODE` is gone.** It was a `constdef` stamp site in `plugin-sharing/src/sharing-rule-service.ts` for a code this package only ever RECOGNISES — `@objectstack/objectql` is the emitter and already carries the row. The per-grant catch now asks `isSystemWriteOrganizationRequiredError(err)`, and the `warn` that reports an absorbed refusal names `SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE`. Both are imported from `@objectstack/objectql`, which exports them for exactly this: a consumer performs the `code` compare without authoring the string, so it acquires no stamp site of its own and cannot drift from what the engine throws.
13+
- **Nothing about the absorbed set moves.** The catch stays as narrow as it was — one engine refusal absorbed, everything else rethrown unchanged — and `plugin-sharing` still emits this code nowhere: the surviving mention is a structured log field on the refusal it just absorbed, not a refusal envelope of its own.
14+
- **No error-code membership moves.** `ERROR_CODE_LEDGER` and `StandardErrorCode` are untouched; `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` stays registered under `@objectstack/objectql` exactly as before. The only ledger change is one `PROVENANCE_WAIVERS` element, 10 waivers → 9, and the gate's site census 339 → 338 with `listed` unchanged at 322.

‎packages/plugins/plugin-sharing/src/sharing-rule-service.ts‎

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
2323
// enforcement hole this closes gets re-opened one seam over.
2424
import { isRowActive } from '@objectstack/core';
2525
// [#14754] The engine's organization refusal for a system write on a
26-
// tenant-scoped object (#8844). Imported as a TYPE only: the runtime check is a
27-
// `code` compare, which is the convention that class itself documents ("a
28-
// caller that catches it identifies it by `code`") so the check survives the
29-
// package boundary where two copies of that module can exist. Typing the
30-
// literal FROM the class is what keeps the two spellings from drifting — a
31-
// typo here would not be a failing test, it would be a catch that never fires.
32-
import type { SystemWriteOrganizationRequiredError } from '@objectstack/objectql';
26+
// tenant-scoped object (#8844). The runtime check is a `code` compare, which is
27+
// the convention that refusal itself documents ("a caller that catches it
28+
// identifies it by `code`") so the check survives the package boundary where
29+
// two copies of that module can exist — `instanceof` is unsound across the dual
30+
// build and fails SILENTLY, as a catch that never fires.
31+
//
32+
// [#14936] The recognizer and the code are taken FROM the engine rather than
33+
// re-spelled here. The engine publishes both precisely so a consumer can
34+
// perform the compare without authoring the string itself: a local literal is a
35+
// second spelling that can drift from what the engine throws, and it acquires a
36+
// `check:error-code-provenance` stamp site in this package for a code this
37+
// package only ever RECOGNISES.
38+
import {
39+
isSystemWriteOrganizationRequiredError,
40+
SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE,
41+
} from '@objectstack/objectql';
3342
import type { SharingEngine } from './sharing-service.js';
3443
import type { SharingService } from './sharing-service.js';
3544
import { normalizeAccessLevel, normalizeStoredAccessLevel } from './access-level.js';
@@ -129,15 +138,6 @@ function rowFromRule(row: any): SharingRuleRow {
129138
};
130139
}
131140

132-
/**
133-
* [#14754] The one engine refusal a reconcile pass absorbs per grant.
134-
*
135-
* Spelled once, and typed from the engine's own declaration so it cannot drift
136-
* from the code the engine actually throws.
137-
*/
138-
const ENGINE_ORGANIZATION_REFUSAL_CODE: SystemWriteOrganizationRequiredError['code'] =
139-
'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED';
140-
141141
/**
142142
* [#14754] What one reconcile pass did, plus the grants the engine's
143143
* organization rule REFUSED in it.
@@ -1649,7 +1649,7 @@ export class SharingRuleService implements ISharingRuleService {
16491649
*
16501650
* `sys_record_share` is tenant-scoped in the #13491 ledger (#14484), so on a
16511651
* walled install an organization-less system insert on it is refused loudly
1652-
* with {@link ENGINE_ORGANIZATION_REFUSAL_CODE}. `SharingService.grant`
1652+
* with {@link SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE}. `SharingService.grant`
16531653
* resolves the organization on every path that can; a platform-global rule
16541654
* (`organization_id = null`, its sweep unscoped) materialising onto an
16551655
* organization-less record resolves none, and meets the refusal.
@@ -1663,8 +1663,9 @@ export class SharingRuleService implements ISharingRuleService {
16631663
*
16641664
* ## Why the catch is narrow, and must stay narrow
16651665
*
1666-
* ONLY {@link ENGINE_ORGANIZATION_REFUSAL_CODE} is absorbed; everything else
1667-
* rethrows unchanged. Two reasons, and the second is measured:
1666+
* ONLY the engine's organization refusal is absorbed — recognised through
1667+
* the engine's own {@link isSystemWriteOrganizationRequiredError}; everything
1668+
* else rethrows unchanged. Two reasons, and the second is measured:
16681669
*
16691670
* - A catch-all would swallow real defects — a driver outage, a criteria
16701671
* bug, a permission failure — and report a pass that "completed" having
@@ -1701,15 +1702,15 @@ export class SharingRuleService implements ISharingRuleService {
17011702
);
17021703
return true;
17031704
} catch (err: any) {
1704-
if (err?.code !== ENGINE_ORGANIZATION_REFUSAL_CODE) throw err;
1705+
if (!isSystemWriteOrganizationRequiredError(err)) throw err;
17051706
this.logger?.warn?.(
17061707
'[sharing-rule] grant refused by the engine organization rule — counted, pass continues',
17071708
{
17081709
rule: rule.name ?? rule.id,
17091710
object: rule.object_name,
17101711
record: recordId,
17111712
recipient: recipientId,
1712-
code: ENGINE_ORGANIZATION_REFUSAL_CODE,
1713+
code: SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE,
17131714
},
17141715
);
17151716
return false;

‎packages/spec/src/api/error-code-ledger.zod.ts‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,21 +1677,6 @@ export const PROVENANCE_WAIVERS: readonly ProvenanceWaiver[] = [
16771677
'protocol layer — the registered emitter — turns it into the 403 the wire carries ' +
16781678
'(ADR-0010 §3.3). Spec ships schemas and pure helpers, never an HTTP door.',
16791679
},
1680-
{
1681-
package: '@objectstack/plugin-sharing',
1682-
code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED',
1683-
registeredUnder: '@objectstack/objectql',
1684-
reason: 'Matches the code, never emits it (#14754, adjudicated on #14937 — maintainer ' +
1685-
'ruling A, 2026-09-04): `ENGINE_ORGANIZATION_REFUSAL_CODE` in ' +
1686-
'`plugin-sharing/src/sharing-rule-service.ts` is a `constdef` the per-grant catch in BOTH ' +
1687-
'reconcile loops compares an incoming `err.code` against, so exactly one engine refusal is ' +
1688-
'absorbed and a refused grant no longer aborts the pass or its stale-row revocations. The ' +
1689-
'emitter is `@objectstack/objectql` (`SystemWriteOrganizationRequiredError`, ' +
1690-
'tenancy/system-write-organization.ts) and the objectql owner key already carries the row ' +
1691-
'(#8844). Recognising a code is not emitting it; the named constant is typed FROM the ' +
1692-
'engine\'s own declaration so it cannot drift from what the engine throws. Removed together ' +
1693-
'with the stamp site when #14936 lands and objectql publishes a recognizer.',
1694-
},
16951680
{
16961681
package: '@objectstack/types',
16971682
code: 'VALIDATION_FAILED',

‎scripts/check-adr-0087-registration.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ import { maskComments, maskCommentsAndLiterals } from './js-comment-mask.mjs';
371371
// #16421 — the DIRECTION ARM, read through the fleet's one declaration reader.
372372
// ⛔ Not re-implemented here: the ruling's condition on the arm is that it has a
373373
// single legal spelling read in a single place, and every reader imports it.
374-
import { readClause2Line } from './pm/check-clause2-carriers.mjs';
374+
import { readClause2Line } from './pm/clause2-line.mjs';
375375
// #16421 — the staging manifest for this gate's own I1/I2 fixture, derived from
376376
// the module graph rather than typed out. Shared with the OTHER site that copies
377377
// this gate into a sandbox (`objectui-changeset-digest.mjs`), because two hand
@@ -7024,8 +7024,8 @@ function selfTest() {
70247024
// ⭐ DERIVED, not listed, and derived by a module BOTH staging sites of this
70257025
// gate import (#16421). It was a two-name list here and the same two-name
70267026
// list in `objectui-changeset-digest.mjs`'s `fw-gate` sandbox. Signal (4)
7027-
// added an edge to `pm/check-clause2-carriers.mjs`, whose own closure is nine
7028-
// modules deep; this site was updated in the same edit and the other was not,
7027+
// added an edge to the clause-② declaration reader, then ten thousand lines
7028+
// of sweep behind it; this site was updated in the same edit and the other was not,
70297029
// and `check:objectui-changeset` went red in CI with an error naming neither
70307030
// the import nor the manifest. One in two is the measured hit rate of "the
70317031
// next author remembers", so neither site holds an opinion about the graph
@@ -7037,7 +7037,7 @@ function selfTest() {
70377037
// moment signal (4) is reached, and the two I-cases below would again report
70387038
// about the wrong thing. This names the one edge that made the list a graph.
70397039
assert(
7040-
staged.includes(copy) && staged.includes('scripts/pm/check-clause2-carriers.mjs'),
7040+
staged.includes(copy) && staged.includes('scripts/pm/clause2-line.mjs'),
70417041
`I1/I2: the staged closure must carry the gate AND the declaration reader it imports — got ${staged.length} file(s): ${staged.join(', ')}`,
70427042
);
70437043
w(

‎scripts/check-changeset-no-major.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ import { isEntrypoint } from './invoked-as.mjs';
391391
// from the gate the PM protocol actually runs. Both modules import node
392392
// builtins only, so this file still runs before `pnpm install`.
393393
import { CONTRACT_REVIEW_LABEL } from './pm/check-half-states.mjs';
394-
import { readClause2Line } from './pm/check-clause2-carriers.mjs';
394+
import { readClause2Line } from './pm/clause2-line.mjs';
395395

396396
const __dirname = dirname(fileURLToPath(import.meta.url));
397397
const REPO_ROOT = resolve(__dirname, '..');
@@ -885,7 +885,7 @@ export function render(result) {
885885
// ① the CLAUSE-② declaration — "this PR puts a new key on a published
886886
// payload" — carried by the `needs:contract-review` gate label and/or by
887887
// the fixed `Clause-②: yes` line the PM protocol spells (read here through
888-
// `check-clause2-carriers.mjs`'s own `readClause2Line`, imported rather
888+
// `clause2-line.mjs`'s own `readClause2Line`, imported rather
889889
// than restated, so the two readers cannot drift);
890890
// ② the CHANGESET LEVEL for the package whose `packages/**/src/**` the diff
891891
// moves.
@@ -1383,7 +1383,7 @@ export function packagesTouched({ cwd, from, head }) {
13831383
// `readClause2Line` returns a REASON with every near miss, and this gate used
13841384
// to print the offending line without it: the reader knew which of three
13851385
// different mistakes the author had made, and the refusal said only "a near
1386-
// miss". The three owe different remedies — `check-clause2-carriers.mjs` says
1386+
// miss". The three owe different remedies — `clause2-line.mjs` says
13871387
// so in its own words ("⛔ The reason changes the sentence, never the state") —
13881388
// so the sentence is what this map supplies.
13891389
//
@@ -1433,7 +1433,7 @@ export function clause2NearMissRemedy(reason, line) {
14331433
}
14341434
return (
14351435
`this gate carries no remedy sentence for a near miss of reason ${JSON.stringify(reason ?? null)} — ` +
1436-
'`readClause2Line` (scripts/pm/check-clause2-carriers.mjs) reports a reason this file has not been taught, ' +
1436+
'`readClause2Line` (scripts/pm/clause2-line.mjs) reports a reason this file has not been taught, ' +
14371437
`and the reason plus the line are printed rather than swallowed. The offending line is: ${line}. Whatever the ` +
14381438
'reason, a declaration is `Clause-②: yes` or `Clause-②: no`, bare, at the start of a line of its own. Add the ' +
14391439
'sentence for this reason to CLAUSE2_NEAR_MISS_REMEDIES in scripts/check-changeset-no-major.mjs.'

‎scripts/objectui-changeset-digest.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,8 +2321,8 @@ function selfTest() {
23212321
// moved.
23222322
//
23232323
// ⭐ It was a hand MANIFEST, and the manifest is what broke (#16421). That
2324-
// gate gained one import — `pm/check-clause2-carriers.mjs`, the fleet's one
2325-
// clause-② declaration reader, whose own closure is nine modules deep — and
2324+
// gate gained one import — the fleet's one clause-② declaration reader, then
2325+
// living inside ten thousand lines of sweep with a nine-module closure — and
23262326
// the author updated the gate's OWN I1/I2 staging site in the same edit and
23272327
// not this one. CI went red HERE, on a gate about objectui changesets, with
23282328
// an error naming neither the new import nor this list. Both sites now DERIVE
@@ -2346,7 +2346,7 @@ function selfTest() {
23462346
for (const rel of gateDeps) gw(rel, readFileSync(join(__dirname, '..', rel), 'utf8'));
23472347
check(
23482348
'#6494 the staged gate carries its whole first-party closure — DERIVED, not a hand manifest',
2349-
gateDeps.includes('scripts/pm/check-clause2-carriers.mjs') && gateDeps.length >= 2,
2349+
gateDeps.includes('scripts/pm/clause2-line.mjs') && gateDeps.length >= 2,
23502350
`${gateDeps.length} dependenc(ies): ${gateDeps.join(', ')}`,
23512351
);
23522352
gg('add', '-A');

0 commit comments

Comments
 (0)