Skip to content

Commit 4916168

Browse files
fix(plugin-sharing, plugin-audit): runtime strings state each decision in words instead of a tracker number (stage 6) (#21533)
Part of #20751 Clause-②: no **Stage 6 of the `domain:services` lane under the maintainer's A / A ruling (5902360492): the `plugin-sharing` strings and the one `plugin-audit` string left over from stage 4.** The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, error `code`, field, route, export or control flow moves (the AST skeleton reads SAME for 6 of 6 changed `.ts` files, below). ## What this does Eight strings in these two packages sent the reader to a tracker number for the reason behind them. In form D, as stages 1 to 5 applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words. All 8 ledgered occurrences in this stage's surface (claim `5962584533`), re-derived from the ledger on `origin/main` at `aa463223` and read again, the same eight, on `ad7c3518` before merging: `plugin-sharing` 7 (7 pairs, 3 files: `share-link-service.ts`, `sharing-rule-service.ts`, `sharing-service.ts`) and `plugin-audit` 1 (`audit-writers.ts`, the entry stage 4 left for a later stage). The file excluded at dispatch, `plugin-sharing/src/translations/serving-seam.test.ts` (PR #21480, since merged), carries no ledger entry and is not touched by this diff. ### Rewritten in words Author- and administrator-visible text first, log lines last. Line numbers are at the head. | Where | Cited | The text now says | Decision read from | |---|---|---|---| | `plugin-audit` `audit-writers.ts:972-976`, the missing-table fix in the audit write-failure line | 5226 | "so on a fresh `os dev` boot the table exists in that sibling file and not in the primary one; look there before concluding it was never created" | 5226's dev measurement and the PM review: the "table never created" premise was falsified; on a fresh `os dev` boot `sys_audit_log` was in the sibling `dev.telemetry.db` (with its rows) and absent from `dev.db`, because ADR-0057 section 3.6 routes the audit lifecycle class to the telemetry datasource | | `sharing-service.ts:312`, the reason the record-share orphan sweep prints in its "revoked N rows" line | 5103 | "every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it" | the ruling on 5103 (option A): a record's deletion revokes every `sys_record_share` row on it, whatever the source, plus a boot sweep by record existence; the card's hazard is a reused record id handing the old record's shares to the new one | | `share-link-service.ts:67`, the same reason for the share-link sweep | 5190 | "a share link is a bearer token, so a reused record id must not inherit it" | 5190's case and ACCEPT: a share link is an identity-less capability token, so on a reused id whoever holds it wins; a deleted record's links are revoked and resolution fails closed | | `sharing-service.ts:735-741`, the write-gate failure log line (`error`) | 6428 | "a failed lookup is a refusal, never an abstention, because an abstention would hand the row to the other write authorities, which may admit it" | 6428's tri-state write verdict (allow / abstain / deny), added because a two-state answer that merged "allow" with "no opinion" was measured failing open, and its fail-closed rule: a query failure is a deny, never an abstain (an abstain hands the row to another authority, a deny ends it) | | `sharing-service.ts:955-961`, the authored-row-write probe log line (`warn`) | 5493 | "(fail-closed: only an app-authored row-level policy that positively admits this row may lift the sharing refusal)" | the maintainer's ruling on 5493 (Q1 = A, Q2 = A1): the by-id write gate defers only on an `admit` from an app-authored, non-floor policy; abstain, a missing method or a throw leaves the refusal byte-for-byte | | `sharing-service.ts:1835-1842`, the hierarchy-scope log line (`warn`) | 5973 | "\"No org\" is not \"every org\": the IHierarchyScopeResolver.resolveOwnerIds contract makes a resolver fail closed on a missing organization" | 5973 is a pull request (merged as `abeb3751f`; its REST endpoints answer 404, its timeline reads): it made `organizationId` the authoritative, required field and wrote the fail-closed rule into `IHierarchyScopeResolver.resolveOwnerIds` | ### Citation only (the sentence already stated the decision) - `sharing-rule-service.ts:424-429`, the no-active-organization refusal (8158: "manage_sharing is an ORG-scoped capability ... answering unscoped would expose every tenant's rules. Select an active organization and retry. Platform operators ... and system contexts are unaffected"). Its `PERMISSION_DENIED` prefix is unchanged. - `sharing-rule-service.ts:529-533`, the platform-global delete refusal (7795: "requires platform authority ... Org-scoped manage_sharing does not authorize it, because this rule belongs to no organization and deleting it revokes every tenant's grants under it. It remains listable, readable and evaluable"). Its `PERMISSION_DENIED` prefix is unchanged. Every cited card (8: 5103, 5190, 5226, 5493, 5973, 6428, 7795, 8158) was read through REST, body and every comment, before its string was rewritten. 5973 answers 404 on both the issue and the pull endpoint; its timeline and its merge commit `abeb3751f` carry the decision. ### The `OrphanSweepSubject.issue` member The two sweep reasons above are the values of `OrphanSweepSubject.issue`, an exported member that `sweepOrphanedRowsByRecordExistence` appends to its "revoked N rows" warning. The member keeps its name and type (a rename is an export change, outside a text-only stage); only its doc comment changes, from "Issue reference appended to ..." to "Why the rows go, appended to ... Runtime text carries no tracker number, so this states the decision in words", so the published declaration no longer asks a caller for a tracker number. ### Translations None of these strings has a locale variant: they are refusal and log text, not translation keys. ## Ledger (`scripts/doc-authoring-prose-id.baseline.json`) The ledger is serial across every lane's stages, so this PR opened only after stage 5's PR #21518 merged (`e3ad4922`). `origin/main` at `ad7c3518` (which also carries stage 4, PR #21472, and another lane's stage, PR #21521) was then merged in (no rebase). The ledger conflicted; main's copy was taken and regenerated on the merged tree with `node scripts/check-doc-authoring.mjs --census-ledger` (exit 0, no growth refusal). Against `main` the diff deletes 16 lines and adds none: exactly the four file blocks of this stage. A scripted key-by-key comparison of main's copy against the regenerated one reads 4 keys moved, all of them this stage's, each to absent; every other row is unchanged. No other open PR touches the file. | | before (`ad7c3518`) | after | |---|---|---| | `plugin-sharing` | 7 occurrences, 7 pairs, 3 files | 0 | | `plugin-audit` (`audit-writers.ts`) | 1 occurrence, 1 pair, 1 file | 0 | | whole ledger | 42 occurrences, 35 pairs, 10 files | 34, 27, 6 (all `service-analytics`) | `pnpm check:doc-authoring` at the head: "sibling-package prose ids hold the baseline — 26 pinned site(s) across 6 file(s), 86200 string(s) read in 1252 parsed source(s), no growth, no burn-down unrecorded". No gate is added or loosened; `scripts/check-doc-authoring.mjs` is untouched. ## Changeset `.changeset/20751-services-strings-stage6-state-the-decision.md`: `patch` for `@objectstack/plugin-sharing` and `@objectstack/plugin-audit`. Measured after building the merged tree: the new sentences are in each package's built output (`dist/index.js` and `dist/index.mjs` of both). A TypeScript scan of every string literal and template text in the built output finds 0 tracker ids in either package. Control: the same scan reads 34 in `service-analytics`' built output, the count its ledger entries carry. ## Text-only proof A TypeScript-AST skeleton of each changed `.ts` file, where every string literal and template text is a placeholder, a run of adjacent string operands of a `+` chain is one string (only its embedded expressions are kept), identifiers and numbers keep their text, and comments are never read. `aa463223` (where the branch was cut) against the head: 6 of 6 SAME, and the same against `ad7c3518` (main did not touch any of the six files). Controls on scratch copies of `sharing-service.ts`, each mutation's marker counted once on disk first: a one-identifier rename reads DIFF; a text-only change reads SAME; a re-split of one string into two concatenated pieces reads SAME. ## Pins - `sharing-service.test.ts:1616`: the write-gate failure log line is found by "an abstention would hand the row to the other write authorities" instead of the id; the `fail-closed` assertion beside it and the `deny` verdict assertions are unchanged. Reverse check, at the committed `90db7d4e` (the merge did not touch these files), through `scripts/ablation-replace.mjs` under the lock: the new clause put back to the citation form (anchor hit once, blob moved) turned exactly that case red (predicted 1, measured 1 of 131); restored byte-identical to `HEAD` with an empty `git diff HEAD`. - No other test asserts any of the eight strings by their ids. The two dogfood tests that read the no-active-organization refusal match "active organization", which stays. ## Tests All through `scripts/pm/os-verify-lock.sh`, every verdict `VERDICT command-exit 0`, at the head `2f545642` (the merged tree; `pnpm install --frozen-lockfile` first): - Build: `turbo run build --filter=./packages/* --filter=./packages/*/*` (71/71). - `@objectstack/plugin-sharing`: 38 files, 954 tests passed; `@objectstack/plugin-audit`: 38 files, 618 tests passed. - `typecheck` for both, including `check:test-typecheck: OK` for each. - The same suites and typecheck were green at `90db7d4e`, before the merge. ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at `2f545642` (8 paths vs merge base `ad7c3518`, 62 changed lines): 74 commands, run one at a time from the worktree after the full build, each exit code recorded before any pipe; 74 exit 0. `--ran`: "74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)". The same 74 also ran green at `90db7d4e`, before the merge. - `check-issue-citations`: "no issue citations added against ad7c351 (5 file(s) read)"; `check:i18n`: "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)"; `check:nul-bytes`: OK, 9876 files; `check:type-check-debt`: "none above its recorded number"; `check:dual-build-cjs-loads`: 106 require entry points across 66 packages load; `check:dts-closure`: 71 built packages, 169/169; `check:sourcemap-no-sources-content`: 68 packages, 532 maps; `check:published-files`: 69 publishable packages; `check:engine-double-contract`: OK. - Outside the derived set, all exit 0 at `2f545642`: the eleven declared wide-population families (`check:init-service-contract`, `check:live-db-isolation`, `check:meta-type-normalized`, `check:optional-error-sink`, `check:resume-authority-declared`, `check:route-envelope`, `check:runner-env-posture`, `check:settings-bind-window`, `check:startup-registry-verdict`, `check:verify-stand-in`, `check:wildcard-fallthrough`), plus `check:durability-log-level` and `check:error-code-casing` (log and refusal text moved; no level or code did). - Lint, narrowed as a measurement: `eslint --no-inline-config --format json` over the 6 changed `.ts` files at `2f545642`: 6 files linted, 0 errors, 0 warnings. `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move any untouched file's verdict. Repo-wide `pnpm lint` is CI's. - `origin/main` was still `ad7c3518` when this PR opened. ## Acceptance notes Noted, not filed: - `OrphanSweepSubject.issue` now carries a reason in words; its name still says "issue". Renaming it is an export change and belongs to no text-only stage. Carrier: none. - `plugin-sharing` test titles and comments still carry ids (`[#6428]`, `#5103`, `#5190`, ...); test bodies and comments are outside the ledger. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 48eb9c1 commit 4916168

8 files changed

Lines changed: 34 additions & 28 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
'@objectstack/plugin-audit': patch
4+
---
5+
6+
Sharing refusals and log lines, and the audit write-failure line, no longer cite tracker numbers; each one states the decision behind it in words
7+
8+
Clause-②: no
9+
10+
Some strings these two packages show to administrators and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
11+
12+
- `@objectstack/plugin-sharing`: the orphan-sweep line for record shares says every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it; the same line for share links says a share link is a bearer token, so a reused record id must not inherit it; the write-gate failure line says a failed lookup is a refusal, never an abstention, because an abstention would hand the row to the other write authorities, which may admit it; the authored-row-write probe line says only an app-authored row-level policy that positively admits the row may lift the sharing refusal; the hierarchy-scope line says the resolver contract makes a resolver fail closed on a missing organization. The two sharing-rule refusals (no active organization; deleting a platform-global rule) drop their citations, since each sentence already says why. The `OrphanSweepSubject.issue` member's doc comment now says the member carries that reason in words.
13+
- `@objectstack/plugin-audit`: the missing-table fix in the audit write-failure line says that on a fresh `os dev` boot the table exists in the sibling telemetry file and not in the primary one, so look there before concluding it was never created.
14+
15+
Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling.

‎packages/plugins/plugin-audit/src/audit-writers.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -971,8 +971,9 @@ function auditWriteFailureLine(f: {
971971
'`OS_SKIP_SCHEMA_SYNC` creates it out-of-band instead). (2) Otherwise it was created on a DIFFERENT ' +
972972
'datasource than the one this write reached: its ADR-0057 §3.6 lifecycle class routes it to the ' +
973973
'dedicated `telemetry` datasource whenever one is registered (`os dev` provisions one by default as a ' +
974-
'SIBLING SQLite file) — see framework#5226. Set `OS_TELEMETRY_DB=0` to keep every lifecycle-classed ' +
975-
'object on the primary datasource.'
974+
'SIBLING SQLite file), so on a fresh `os dev` boot the table exists in that sibling file and not in the ' +
975+
'primary one; look there before concluding it was never created. Set `OS_TELEMETRY_DB=0` to keep ' +
976+
'every lifecycle-classed object on the primary datasource.'
976977
: 'Fix: resolve the driver fault quoted at the head of this line on the connection this write ran ' +
977978
'on — every audited write that hits it loses its row until it is resolved.';
978979
return consequence + once + fix;

‎packages/plugins/plugin-sharing/src/record-orphan-cleanup.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ export interface OrphanSweepSubject {
118118
table: string;
119119
/** Noun for log messages: `share` → "orphan share sweep", "share rows". */
120120
noun: string;
121-
/** Issue reference appended to the "revoked N rows" warning. */
121+
/**
122+
* Why the rows go, appended to the "revoked N rows" warning. Runtime text
123+
* carries no tracker number, so this states the decision in words.
124+
*/
122125
issue: string;
123126
}
124127

‎packages/plugins/plugin-sharing/src/share-link-service.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;
6464
const SHARE_LINK_SWEEP_SUBJECT = {
6565
table: 'sys_share_link',
6666
noun: 'share-link',
67-
issue: '#5190',
67+
issue: 'a share link is a bearer token, so a reused record id must not inherit it',
6868
} as const;
6969

7070
/** URL-safe alphabet (RFC 4648 base64url minus padding). 64 symbols. */

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ export class SharingRuleService implements ISharingRuleService {
424424
'PERMISSION_DENIED: sharing-rule administration requires an active organization — this ' +
425425
'session carries none. manage_sharing is an ORG-scoped capability (ADR-0111 D6), so with ' +
426426
'no organization resolved there is no tenant whose rules it authorizes, and answering ' +
427-
'unscoped would expose every tenant’s rules (#8158). Select an active organization and ' +
427+
'unscoped would expose every tenant’s rules. Select an active organization and ' +
428428
'retry. Platform operators (manage_platform_settings or the platform_admin position) and ' +
429429
'system contexts are unaffected.',
430430
);
@@ -529,7 +529,7 @@ export class SharingRuleService implements ISharingRuleService {
529529
'PERMISSION_DENIED: deleting a platform-global sharing rule requires platform authority — ' +
530530
'the manage_platform_settings capability or the platform_admin position. Org-scoped ' +
531531
'manage_sharing does not authorize it, because this rule belongs to no organization and ' +
532-
'deleting it revokes every tenant’s grants under it (#7795). It remains listable, ' +
532+
'deleting it revokes every tenant’s grants under it. It remains listable, ' +
533533
'readable and evaluable.',
534534
);
535535
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1613,7 +1613,7 @@ describe('[#6428] fail-closed: an unresolvable verdict is DENY, never abstain',
16131613

16141614
expect(logged.length).toBeGreaterThan(0);
16151615
expect(String(logged[0][0])).toContain('fail-closed');
1616-
expect(String(logged[0][0])).toContain('#6428');
1616+
expect(String(logged[0][0])).toContain('an abstention would hand the row to the other write authorities');
16171617
});
16181618

16191619
it('a throwing SHARE lookup denies too — the whole evaluation is covered, not just the first query', async () => {

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export interface SharingSecurityProbe {
309309
const RECORD_SHARE_SWEEP_SUBJECT = {
310310
table: 'sys_record_share',
311311
noun: 'share',
312-
issue: '#5103',
312+
issue: 'every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it',
313313
} as const;
314314

315315
/**
@@ -734,8 +734,9 @@ export class SharingService implements ISharingService {
734734
): SharingWriteVerdict {
735735
this.logger?.error?.(
736736
`[sharing] the ${verb} gate could not resolve a verdict for '${object}' record `
737-
+ `'${recordId}' (user ${context?.userId ?? 'unknown'}) — DENYING (fail-closed, #6428): `
738-
+ 'a failed lookup is a refusal, never an abstention',
737+
+ `'${recordId}' (user ${context?.userId ?? 'unknown'}) — DENYING (fail-closed): `
738+
+ 'a failed lookup is a refusal, never an abstention, because an abstention would hand the row '
739+
+ 'to the other write authorities, which may admit it',
739740
err instanceof Error ? err : new Error(String(err)),
740741
);
741742
return 'deny';
@@ -954,7 +955,8 @@ export class SharingService implements ISharingService {
954955
this.logger?.warn?.(
955956
`[sharing] the authored-row-write probe for '${object}' record '${recordId}' `
956957
+ `(${operation}, user ${context?.userId ?? 'unknown'}) could not be resolved — `
957-
+ 'ABSTAINING, so the existing refusal stands (fail-closed, #5493)',
958+
+ 'ABSTAINING, so the existing refusal stands (fail-closed: only an app-authored row-level '
959+
+ 'policy that positively admits this row may lift the sharing refusal)',
958960
err instanceof Error ? err : new Error(String(err)),
959961
);
960962
return 'abstain';
@@ -1833,7 +1835,8 @@ export class SharingService implements ISharingService {
18331835
this.logger?.warn?.(
18341836
'[sharing] hierarchy scope NOT widened: an organization wall is in force but the caller ' +
18351837
'context carries no active organization — failing closed to owner-only. ' +
1836-
'"No org" is not "every org" (IHierarchyScopeResolver.resolveOwnerIds, #5973); ' +
1838+
'"No org" is not "every org": the IHierarchyScopeResolver.resolveOwnerIds contract makes a ' +
1839+
'resolver fail closed on a missing organization; ' +
18371840
'the same rule walls Layer 0 (ADR-0095 D1 / ADR-0105 D1).',
18381841
{ userId: me, scope },
18391842
);

‎scripts/doc-authoring-prose-id.baseline.json‎

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,4 @@
11
{
2-
"packages/plugins/plugin-audit/src/audit-writers.ts": {
3-
"#5226": 1
4-
},
5-
"packages/plugins/plugin-sharing/src/share-link-service.ts": {
6-
"#5190": 1
7-
},
8-
"packages/plugins/plugin-sharing/src/sharing-rule-service.ts": {
9-
"#7795": 1,
10-
"#8158": 1
11-
},
12-
"packages/plugins/plugin-sharing/src/sharing-service.ts": {
13-
"#5103": 1,
14-
"#5493": 1,
15-
"#5973": 1,
16-
"#6428": 1
17-
},
182
"packages/services/service-analytics/src/analytics-service.ts": {
193
"#3867": 1,
204
"#5222": 1,

0 commit comments

Comments
 (0)