Repository navigation
fix(automation, audit): runtime strings state each decision in words instead of a tracker number (stage 4) - #21472
Conversation
…instead of a tracker number (stage 4) The service-automation refusals, prescriptions, log lines and run-object field help, and the sys_activity type help, drop their tracker numbers; where the sentence leaned on the number it now says what was decided. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…sed source text The es-ES, ja-JP and zh-CN leaves were byte copies of the English source; they were emptied and refilled by the extract config's own documented command (scripts/check-i18n-bundles.mjs --write --filter=plugin-audit), which also rewrote en and recorded the new source digests. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…age 4 (service-automation, plugin-audit) Recomputed with `node scripts/check-doc-authoring.mjs --census-ledger` on the tree with origin/main merged in: 58 lines deleted, 0 added; the service-automation entries and the plugin-audit entries other than audit-writers.ts go to zero, no other row moves. Adds the stage-4 changeset. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ab498c8c92e369a84c5e9b06dd2d8c58278bdff8 && git checkout ab498c8c92e369a84c5e9b06dd2d8c58278bdff8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa4632235ba571ef800b95e6bc18d00a30aa1d57 f300a255b0e20023e6f6e0e131fc5b7b261ac4d8 && git checkout -B drift-repro aa4632235ba571ef800b95e6bc18d00a30aa1d57 && git merge --no-ff f300a255b0e20023e6f6e0e131fc5b7b261ac4d8
node scripts/docs-audit/affected-docs.mjs --json aa4632235ba571ef800b95e6bc18d00a30aa1d57
|
…registry findings state each decision in words instead of a tracker number (stage 2) (objectstack-ai#21491) Part of objectstack-ai#20749 Clause-②: no **Stage 2 of the `domain:spec` lane's share under the maintainer's A / A ruling (5902360492): the last 51 ledgered tracker-number occurrences in `packages/lint`, in 18 `packages/lint/src` files.** After this, `packages/lint` has no row left in `scripts/doc-authoring-prose-id.baseline.json`. The card stays open for the later stages, so this PR carries no closing keyword. Text only: no rule, condition, code path, rule id, severity or registry field moves (AST skeleton proof below, 18 of 18 SAME). ## What this does These 18 files print findings to authors through `os validate`, `os lint` and `os build` (and, for the rules on the runtime publish gate, through the Studio, REST and MCP publish door). `authoring-rules.ts` also carries the `surfaceReason` texts of the exported `AUTHORING_RULES` registry, and `lint-liveness-properties.ts` one thrown integrity error. 51 tracker numbers in that text sent the reader to a card for the reason behind it. In form D, as stage 1 (PR objectstack-ai#21462) and the sibling lane's stages 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. The surface, re-derived from the ledger and from `check-doc-authoring.mjs --census` on `origin/main` at `7e7e64b13d` (the claim's numbers hold exactly): `authoring-rules.ts` 9, `validate-empty-combinators.ts` 6, `validate-predicate-path-refs.ts` 6, `validate-searchable-fields.ts` 5, `validate-sortable-fields.ts` 5, `validate-null-guards.ts` 4, `validate-visibility-predicates.ts` 3, `data-model-rules.ts` 2, `validate-security-posture.ts` 2, and one each in `lint-liveness-properties.ts`, `validate-component-props.ts`, `validate-dashboard-action-refs.ts`, `validate-nav-object-servability.ts`, `validate-react-page-props.ts`, `validate-rule-schema-formats.ts`, `validate-seed-state-machine.ts`, `validate-view-containers.ts` and `validate-widget-bindings.ts`. They sit in 46 census sites (40 string groups once a `+` chain is read as one string). By class: 37 occurrences in finding envelopes (a `message` or `hint`), 13 in prose (9 in `AUTHORING_RULES` `surfaceReason` texts, 4 in the null-guard outcome clauses a finding carries) and 1 thrown. None is logged. Every cited card (32 objectstack cards, plus `objectui#4049`, `objectui#2348` and `objectui#4051`, where objectstack-ai#5149 was migrated) was read through REST, body and every comment, before its string was rewritten. Where a card closed with no ruling comment, its landing commit was read. ### Rewritten in words Lines are the census lines at `7e7e64b13d`. | Where | Cited | The text now says | Decision read from | |---|---|---|---| | `authoring-rules.ts:440`, `:1177`, `:1974`, the full-snapshot, capability-reference and sharing-rule `surfaceReason` texts | 4463 | "P2 of the runtime publish gate (the Studio, REST and MCP door that runs this registry):" in place of "P2 (objectstack-ai#4463):" | 5156970572 (the maintainer-delegated ruling: one shared rule core, run at the metadata write boundary so Studio, REST and MCP are all covered), claim 5160065194 (P1 flow first, P2 the full snapshot), ACCEPT 5160403203 | | `authoring-rules.ts:507`, the advisory-volume reason | 4717, 4716 | "each carried back in the save response and rendered by Studio's designer ... The object door opened to the gating object rules alone; crossing an advisory one is a separate UX decision." | 4717: maintainer ruling 5237206591 (`advisories` rides the save response; Studio renders it); 4716: adjudication 5328603673 (the five gating object rules cross, the six advisory-only rules do not ride, advisory UX is separate work) | | `authoring-rules.ts:1111`, the component-types reason | 4716 | "pending the crossing discipline the gating object rules went through: a measured false-refusal budget, here over stored tenant page rows" | 5328603673 (crossing priced on a measured false-refusal budget) | | `authoring-rules.ts:1976`, the sharing-rule reason | 9698 | "The anchor arm, which refuses a rule anchored on a public-OWD object or a master-detail detail (no share row could widen either), FALSIFIED that" | triage promotion 5332890862 (a statically decidable inert sharing declaration is a loud authoring error, as objectstack-ai#7503 ruled) and the landing dev report 5338330042 | | `data-model-rules.ts:344`, bare declared `unique: true` | 5082 | "Protocol 18 rejects this spelling, and stored metadata that still carries it converts to `unique: 'global'`, which builds the same physical index." | the card body (ADR-0120 D2 conversion `true` to `'global'`, bare `true` rejected at protocol 18) and the hold records 5225701234 and 5481784294 | | `lint-liveness-properties.ts:288`, unrecognised ledger status (thrown) | 11384 | "An unrecognised status fails loudly here rather than being graded `dead`." | claim 5389729619 and dev report 5390065066 (an unrecognised status throws instead of silently grading `dead`) | | `validate-component-props.ts:326`, props hint | 5068 | "props are judged here, at the authoring door, as a warning before they become an error" | maintainer ruling 5196404940 (direction A: gate at publish/lint by `type`, warning first then error; B, reshaping `properties`, not done) | | `validate-empty-combinators.ts:231`, `:235`, `:271`, the `$and: []`, `$or: []` and empty-node messages | 5322 | "Every backend reduces an empty combinator (an empty node) to its boolean identity, so it ..." | maintainer ruling 5185589944 (boolean identity reduction everywhere; the analytics compilers align with the five backends) | | `validate-empty-combinators.ts:237`, `$or: []` on a read scope | 5134 | "(fail-closed by design: an empty disjunction never opens a read scope to the whole table)" | the card body and ACCEPT 5178806144 (an empty `$or` compiles to FALSE; the SQL driver had answered the whole table) | | `validate-null-guards.ts:597`, the fail-closed outcome | 4649, 4763 | "(a predicate that cannot evaluate refuses the write rather than being skipped)" | 4649: PM ruling 5161512629 and ACCEPT 5161923543 (fail closed on a predicate that cannot evaluate); 4763: closing record 5164195111 (the `has()` shape refused at build/publish) | | `validate-predicate-path-refs.ts:652`, `:694`; `validate-visibility-predicates.ts:1001`, `:1023`, `:1126`, the fall-open consequence | 5149 (and 6254 at `:694`) | "(failing open is the console's settled behaviour)" | 5149: ruling 5202273981 (keep fail-open, make it loud, lint at build time) and, on the migrated `objectui#4051`, the delegated ruling 5235422835 (fail-open kept); 6254: landing `643b7c76bc` (a repeater still spells its root `data`, which the `:694` hint already states) | | `validate-predicate-path-refs.ts:731`, dotted right-hand side | `objectui#4049` | "the form evaluator keeps its right-hand side a literal by design, and says why only in a development build" | claim and ruling 5251302658 (B: a dev-mode diagnostic, no semantic change; A rejected) and ACCEPT 5251653400 | | `validate-rule-schema-formats.ts:326`, format hint | 5029 | "the one `rule-validator.ts` registers so that a `format` is enforced on every write" | PM ruling 5175865103 (option 1: register `ajv-formats`) and ACCEPT 5176328955 | | `validate-security-posture.ts:464`, unset OWD | `objectui#2348` | "the leave_request incident, where an object with no sharingModel let an ordinary read/write grant read and edit every other user's records" | the PR body (merged) and ADR-0090 §Context and D1, which record the incident | | `validate-security-posture.ts:526`, `controlled_by_parent` with no relation | 7474 | "refused with 422 INVALID_METADATA, as a metadata defect rather than a permission denial" | maintainer ruling 5248468777 (split the legs by true semantics) and ACCEPT 5248935610 | | `validate-seed-state-machine.ts:146`, seed outside the state machine | 3433 | "(a seed records established facts rather than walking the lifecycle)" | landing commit `0c302a7790` (seed writes exempt from `state_machine`: a curated snapshot of established facts) | | `validate-view-containers.ts:133`, a ViewItem in `views:` | 5320 | "The registration loop refuses this entry too: the stack schema, this rule and the loop hold `views:` to one container-only contract." | ruling 5265947108 (tighten the loop to the container-only contract) and fork ruling 5271008099 (the `viewItems:` home first, then the tighten with lint alignment) | | `validate-react-page-props.ts:458`, absent `groupBy` | 5583 | "An ungrouped single-value chart is NOT a supported ObjectChart shape — groupBy stays required, ..." (the ruling stated directly, its citation dropped) | landing commit `e0f300ba5d` (`groupBy` stays required; the single-value need is served by the object-metric block) | ### Citation only (the sentence already stated the decision) - `authoring-rules.ts`: `:1978` (8309, "`objects` IS carried by the per-write snapshot"; audit 5279528993); `:1982` (7220, "a rule that has never run at a door should not claim it"; maintainer ruling 5237225765). - `data-model-rules.ts:521` (5030, "SQL UNIQUE is NULL-distinct ... it enforces nothing", the fix naming the NULL-safe organization key part; ADR-0120 and landing record 5178627933). - `validate-dashboard-action-refs.ts:299` (`objectstack#6739`, "a modal target names a PAGE, only", with the hint naming `actionType: 'form'`; maintainer ruling 5229995836). - `validate-empty-combinators.ts:256` (5330, the hint already says a programmatic producer keeps the runtime identity and only authored metadata is judged; ACCEPT 5226245518); `:295` (5297, "which is why the runtime absorbs rather than filters"). - `validate-nav-object-servability.ts:167` (7912, "The server prunes this entry from the served `/meta` payload"; maintainer ruling 5271014830). - `validate-null-guards.ts:601` (4811, the clause already describes the skipped `requiredWhen`; ACCEPT 5169421708). - `validate-predicate-path-refs.ts:737` (`objectui#4049`, "it stops working when this surface moves to the real CEL evaluator"); `:739` (7696, "this one finding carries BOTH readings"; dev report 5255289068). - `validate-searchable-fields.ts:356`, `:419`, `:443`, `:476` (4254, "400 INVALID_FIELD"; landing `af2a0958c8`); `:414` (6674, the hint already says the ingress gate now refuses the entry; triage promotion 5230241805 and landing `4ac12ef4cf`). - `validate-sortable-fields.ts:415`, `:416`, `:435`, `:471` (6994 and 7095, "the REST ingress and the engine" refuse a formula sort with 400 INVALID_SORT; landings `9f7a7c25ab` and `6908830573`, maintainer ruling 5236143229 on 7095). - `validate-widget-bindings.ts:982` (2501, "The filter is ANDed into this widget's analytics query", the same treatment this file's two neighbouring messages already had). No occurrence was left in place: no cited decision was unclear, and each fit the string's space. ## Ledger (`scripts/doc-authoring-prose-id.baseline.json`) Recomputed with `node scripts/check-doc-authoring.mjs --census-ledger` (exit 0, no growth refusal) into a scratch file, then copied into place; recomputed again on the merged tree (below), where it is byte-identical to the file in this branch. Against `main` the diff deletes 71 lines and adds none: exactly the 18 file blocks (36 brace lines and 35 pair lines). A scripted comparison of every other key: 0 moved, 0 added. | | before | after | |---|---|---| | the 18 files | 51 occurrences, 35 pairs, 18 files | 0 | | `packages/lint` | 51 occurrences, 18 files | 0 | | whole ledger, at the branch point `7e7e64b13d` | 174 occurrences, 127 pairs, 53 files | 123 occurrences, 92 pairs, 35 files | | whole ledger, on `main` at `cba4297176` (merged in) | 136 occurrences, 97 pairs, 39 files | 85 occurrences, 62 pairs, 21 files | `pnpm check:doc-authoring` at the merged head `dfb89505f5`: "sibling-package prose ids hold the baseline — 72 pinned site(s) across 21 file(s), 86353 string(s) read in 1253 parsed source(s), no growth, no burn-down unrecorded". No gate is added or loosened; `scripts/check-doc-authoring.mjs` is untouched. Ledger serial: while this stage was built, the sibling lane's stage 4 (PR objectstack-ai#21472) held the ledger. This PR opened only after it merged (`cc0786223b`), with `origin/main` at `cba4297176` merged in (a merge commit, no rebase) and the ledger recomputed on that tree; git's text merge of the two deletion-only diffs and the recomputation agree byte for byte. At opening, no open PR touches the ledger or any of the 18 files (every open PR's file list read). PR objectstack-ai#21480 (objectstack-ai#21457) landed in the same queue run; its edits to `validate-predicate-path-refs.test.ts` and `lazy-deps.test.ts` are kept as they landed (both files equal `main` here), and this PR touches neither. ## Quoted elsewhere - `content/docs/data-modeling/schema-design.mdx:179` quoted the stale-entry hint verbatim, `(objectstack-ai#4254)` included; the quote is updated to the new text in this PR. - `skills/**`: no quote of a changed message. - The other docs hits for these numbers are the docs' own prose with their own citations, not quotes of lint text; untouched. ## Changeset `.changeset/20749-lint-strings-stage2-state-the-decision.md`: `patch` for `@objectstack/lint`, carrying `Clause-②: no`. Measured after building: the new sentences are in `packages/lint/dist/index.js`, `index.cjs`, `runtime.js` and `runtime.cjs`, and none of the replaced id-bearing fragments is (the two `(objectstack-ai#6994)` / `(objectstack-ai#5082)` hits left in `dist` are source comments tsup keeps). ## Text-only proof The stage-1 tool, unchanged: a TypeScript-AST skeleton of each changed source in which every string literal and template text is a placeholder, a `+` chain is flattened and a run of adjacent string operands is one string (only its embedded expressions are kept, in order), identifiers, numbers and regex literals keep their text, every child is visited, and comments are never read. A second leg compares the TEXT of every string group in order: each group that changed must have carried a tracker id before and carry none after, and every other group must be byte-identical. `7e7e64b13d` against the head: 18 of 18 SAME on both legs, token counts identical per file, 40 groups changed, all of them id-bearing before and id-free after, parse diagnostics 0/0. Controls on a scratch copy of `validate-empty-combinators.ts` (head version), each mutation counted on disk first (1 anchor hit, replacement present, anchor gone): one function renamed reads DIFF; `===` flipped to `!==` reads DIFF; one literal re-split into two `+` operands reads SAME with no extra group changed; a text change in a string that never carried an id reads SAME on the skeleton and VIOLATION on the text leg. No repo file was mutated for the controls. ## Pins Every assertion that found a finding by its tracker number, or by the full old text, now finds it by the words that replaced it, at the same strength (`toContain` stays `toContain`, the exact `toBe` stays exact). No rule-id, severity or path assertion was touched. - `validate-visibility-predicates.test.ts:968`, `:1418`: "failing open is the console's settled behaviour" for `'objectstack-ai#5149'`. - `validate-react-page-props.test.ts:1202`: "groupBy stays required, and a single number belongs in an object-metric block instead" for `'5583'`. - `validate-searchable-fields.test.ts:760`: the exact outside-the-declared-set message, now ending "returns 400 INVALID_FIELD." - `packages/cli/test/data-model-rules.test.ts:622`: "on every row whose 'organization_id' is NULL it enforces nothing" for `'objectstack-ai#5030'`. ## Tests All builds and tests through `scripts/pm/os-verify-lock.sh`, every verdict `VERDICT command-exit 0` on the runs quoted. These ran at `d5137b469b`, the head before `main` was merged in; the merge brought nine `main` commits that touch none of this PR's files apart from other rows of the ledger, and the build, the lint suite and the gate union are re-run on the merged head, with their readings in this stage's dev report on objectstack-ai#20749. - Build: `pnpm --workspace-concurrency=2 --filter '@objectstack/lint^...' build` (the closure), then `pnpm turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*` ("Tasks: 71 successful, 71 total") for the dist-reading gates and the CLI test. - `@objectstack/lint`: `vitest run --maxWorkers=2`: "Test Files 119 passed (119) / Tests 5614 passed | 5 skipped (5619)". The first run, before the re-pins, read 4 failed in 3 files, exactly the four pins listed above. `typecheck`: `tsc --noEmit` exit 0 and "check:test-typecheck: OK — @objectstack/lint's test layer compiles ... 2 file(s) / 6 error(s) / 2 pinned signature(s) held". - `@objectstack/cli`, unit layer, `test/data-model-rules.test.ts`: "Test Files 1 passed (1) / Tests 56 passed (56)". No `packages/cli` source, `bin/` or integration-layer file is touched, so the integration layer is CI's. ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at `d5137b469b`: 99 commands, run one at a time from the worktree, each exit code recorded before any pipe. `--ran`: "99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a DERIVED zero — all 99 recorded an exit code and none of them is 3)". All 99 exit 0, the dist-reading ones (`check:docs-transcript-drift`, `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:published-files`, `check:sourcemap-no-sources-content`) after the full build. Outside the derived set, also all exit 0: the 11 declared wide-population families and the 11 artifact-roster families whose roster sits under one of this PR's directories (`check-changeset-fixed`, `check-published-list-mirrors` and its self-test, `check:authz-resolver`, `check:console-injection`, `check:engine-double-contract`, `check:error-code-casing`, `check:filter-alias-parity`, `check:i18n-stale-fill`, `check:published-readme-exports`, `check-dts-references --self-test`). - Named by the dispatch: `check:doc-authoring` (self-test and run) exit 0; `check:nul-bytes` exit 0; `check-adr-0087-registration` exit 0 ("this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)"); `check:empty-changeset` exit 0; `check-changeset-fixed` exit 0 (""fixed" group is in sync with 69 public workspace packages"); `check-changeset-no-major` exit 0 on the plain run ("This diff introduces no `major` bump."), and exit 0 when fed this body as a `pull_request` event (the `Clause-②: no` line read, `patch` judged against it). At the merged head `dfb89505f5`, `check:doc-authoring`, `check:nul-bytes`, `check-changeset-no-major` and `check-adr-0087-registration` were re-run before opening: all exit 0. - `check-issue-citations`: "no issue citations added against 7e7e64b (18 file(s) read)"; `check:partof-closing-keyword` on this body: exit 0. - Repo-wide `pnpm lint` is CI's. ## Acceptance notes Noted, not filed: - Test titles and test-only strings in these files' tests still cite numbers, outside stage 2: 228 ids in test titles and 21 in other test strings across `lint-liveness-properties.test.ts` (35 / 4), `validate-component-props.test.ts` (19 / 0), `validate-dashboard-action-refs.test.ts` (8 / 0), `validate-empty-combinators.test.ts` (3 / 1), `validate-null-guards.test.ts` (3 / 0), `validate-predicate-path-refs.test.ts` (11 / 1), `validate-react-page-props.test.ts` (28 / 0), `validate-rule-schema-formats.test.ts` (4 / 0), `validate-searchable-fields.test.ts` (13 / 0), `validate-security-posture.test.ts` (7 / 1) and its `.runtime-surface.test.ts` (27 / 2), `validate-seed-state-machine.test.ts` (1 / 0), `validate-sortable-fields.test.ts` (12 / 0), `validate-view-containers.test.ts` (2 / 0), `validate-visibility-predicates.test.ts` (23 / 0), `validate-widget-bindings.test.ts` (27 / 0), plus `authoring-rule-wiring.test.ts` (1 / 12) for `authoring-rules.ts` and `reference-integrity-suite.test.ts` (4 / 0) for `validate-nav-object-servability.ts`, which have no same-named test file; `data-model-rules`' two same-named test files carry none, and `packages/cli/test/data-model-rules.test.ts` carries 3 in titles. The ledger does not read test files. - `validate-empty-combinators.test.ts` selects its identity cases with `(c.note ?? '').includes('objectstack-ai#5322')` over `FILTER_LOGIC_CASES` notes in `@objectstack/spec`: that test-only id is load-bearing (it is a selector, not prose), so the later stage that sweeps test strings cannot just strip it. - `authoring-rules.ts:1111`'s reason holds the component-types rule to a budget measured "over stored tenant page rows"; the object-door crossing it cites was adjudicated on a measured lower bound from authored config-file metadata, with a post-launch replay as the audit, because no tenant rows exist yet. The reason's wording is unchanged in substance here; whether that bar still describes the page crossing is the seat's to judge. Carrier: none. - The two `.mjs` gate scripts stage 1 named (`packages/lint/scripts/check-doc-formula-expressions.mjs`, `scripts/check-startup-registry-verdict.mjs`) are untouched, as staged. - Comments in the 18 files keep their ids: a comment is the sanctioned home for an internal anchor. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…n in words instead of a tracker number (stage 6) (objectstack-ai#21533) Part of objectstack-ai#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 objectstack-ai#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 objectstack-ai#21518 merged (`e3ad4922`). `origin/main` at `ad7c3518` (which also carries stage 4, PR objectstack-ai#21472, and another lane's stage, PR objectstack-ai#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 (`[objectstack-ai#6428]`, `objectstack-ai#5103`, `objectstack-ai#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>
Part of #20751
Clause-②: no
Stage 4 of the
domain:serviceslane under the maintainer's A / A ruling (5902360492): theservice-automationandplugin-auditstrings. The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, errorcode, field, route, export or control flow moves (AST skeleton reads SAME for 10 of 10 changed sources, below).What this does
The two packages' refusals, prescriptions, warnings, log lines, the
sys_automation_run/sys_flow_dispatchfield help and description, and thesys_activitytypehelp (with itses-ES,ja-JPandzh-CNleaves) sent the reader to a tracker number for the reason behind them. In form D, as stages 1 to 3 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 38 ledgered occurrences in this stage's surface (claim
5959898258), re-derived from the ledger onorigin/mainat9b7a0ef3:service-automation33 (25 pairs, 9 files),plugin-audit5 (5 pairs, 5 files:objects/sys-activity.object.tsand its fourtranslations/*.objects.generated.ts). They sit in 37 string sites. The sixthplugin-auditledger entry,audit-writers.ts(one occurrence), is in a file excluded at file level (open PR #21436) and stays for a later stage. None of the three excluded files (audit-writers.ts,stored-metadata-body-migration.ts,stored-metadata-hash-migration.test.ts) is touched.Rewritten in words
Author- and administrator-visible text first, log lines last. Line numbers are at the head.
engine.ts:196-205, thefieldValuesrefusal prescriptions oncreate_recordandupdate_recordfieldskey rather than two spellings"cfg.fields ?? cfg.fieldValuesalias in the executor; the design review declined it: correct the metadata at the source and keep a single strictconfig.fieldscontractengine.ts:210-213, the screen fieldvisibleIfrefusal prescriptionrequiredfield meant to stay hidden then blocks the screen from ever being submitted"requiredfield the author meant to hide rendered unconditionally and the screen flow could not be submitted;visibleWhenwent into the contract and is forwarded verbatimengine.ts:10272-10274, the undeclared-config-key refusalconfigSchemagot a Zod written from its executor and a two-way key-set reconciliation (CRUD, screen and map surfaced seven read-but-undeclared keys, all declared)builtin/template.ts:192, the unknown value-expression function errornullrewrite of an unknown function name became a loud, named errorsys-automation-run.object.ts:169,node_typehelprefuseGatedResumeenforces it: the gate is keyed on what the run is parked on; anapprovalpause continues only throughApprovalService, while ascreenpause stays open to its flow runnersys-automation-run.object.ts:234trigger_typeand:288trigger_record_idhelpsys-automation-run.object.ts:324,steps_jsonhelpplugin-auditobjects/sys-activity.object.ts:116-121,typehelp, and its four locale leavesplugin.ts:1680-1689, the inert-connector warningproviderare catalog descriptors (descriptor-only contract) ... An entry that names aprovideris a connector instance instead: that provider's installed executor materializes it into a live connector (ADR-0097)"enabled: falsemarks a catalog-only entry) and 2977's (ADR-0097 implemented: a provider-bound entry is materialized at boot by a generic executor). The old sentence said provider-bound instances were "tracked in" 2977, which has since landed;findInertDeclaredConnectorsalready skips provider-bound entries, which the new wording now saysplugin.ts:1143-1146, debugregisterFlowthrough an opt-in object-schema resolverplugin.ts:1184-1187, debugrunAs:'user'resolves the triggering user's positions and permission sets at run setupplugin.ts:1214-1217, debugconfig.expand, expanded engine-side as the run's own identity so the referenced object's RLS/FLS holdsCitation only (the sentence already stated the decision)
builtin/connector-nodes.ts:72(3017, "Dispatch is unavailable until its upstream recovers; the platform retries automatically." — ADR-0097 quotes "the platform retries automatically", which stays verbatim).builtin/parse-config.ts:112(4277, "config does not satisfy the X contract ... The declared contract is the node type's configSchema (the Studio form) and the X config Zod").builtin/screen-nodes.ts:336(1870, "no function named 'X' is registered. Register it viadefineStack({ functions }), or fix the name").engine.ts:181,:184,:187,:191, the bulk-intent guidance (5393, "Bulk intent ismulti: true... so the concept keeps one name from node config to driver call ... a predicate write is refused by the engine rather than silently widened"; "Translating that declaration intooptions.multion the engine call is the executor's job").engine.ts:4026(3017, "the materializer retries with backoff").engine.ts:6069,:6073(3760, 1888: "its data operations will be REFUSED ... Declare runAs:'system' ... a record-change flow fired by a system write carries no user either (ADR-0049)").engine.ts:10268(4277, "Flow 'X' rejected: N undeclared config key(s)").engine.ts:11326(4414, "The branch selection is IGNORED and every out-edge is evaluated instead ... mark the fallback edgeisDefault: true").runtime-identity.ts:113, theAUTOMATION_UNSCOPED_RUN_DATA_ACCESSrefusal (1888, 3760: "refusing a data operation ... would execute UNSCOPED ... DeclarerunAs: 'system'"; it keeps "(ADR-0049)"). Itscodeis unchanged.plugin.ts:1831,:2074,:2086,:2143(3017: "pending retry cancelled", "until a retry succeeds", "retrying with backoff, attempt N", "next retry in Nms").sys-flow-dispatch.object.ts:86(10220, "one row per claimed dispatch key ... so a re-scan, a rebuilt kernel or an operator replay never re-launches a flow for a window it already delivered").Every cited card (21: 1870, 1888, 1928, 2419, 2585, 2612, 2977, 3017, 3356, 3475, 3528, 3760, 3801, 4045, 4277, 4414, 5393, 7533, 10220, 11060, 11507) was read through REST, body and every comment, before its string was rewritten. 2419 is a pull request; its closing comment carries the decision. 11060 and 11507 answer 404 on the issue endpoint while their comment threads read normally; the rulings and dev reports in those threads carry the decisions.
Translations
es-ES,ja-JPandzh-CNsys_activity.fields.type.helpleaves were byte copies of the English source, recorded as such in each locale's*.source-hashes.generated.ts. No translated text was written by hand: the three leaf values were emptied (key kept) andnode scripts/check-i18n-bundles.mjs --write --filter=plugin-audit(the extract config's documented command,--fill=default) refilled them from the revised source, rewroteen, and recorded the three new digests. The same route the original ruling's PR took for these leaves.sys-activity.object.ts). Tracker numbers left in the package's locale bundles: 0.Ledger (
scripts/doc-authoring-prose-id.baseline.json)At dispatch no open PR held this file. While this branch was being built, PR #21462 (
domain:specstage 1 of #20749) opened and took it, so this PR waited: the branch was finished and green, and it was opened only after #21462 merged (7e7e64b1, about 40 minutes of waiting). Thenorigin/mainat7e7e64b1was merged in (no rebase), and the ledger was recomputed on that tree withnode scripts/check-doc-authoring.mjs --census-ledger(exit 0, no growth refusal) into a scratch file. The recomputed file is byte-identical to git's textual merge of the two sides. Againstmainthe diff deletes 58 lines and adds none: exactly the 14 file blocks of this stage. Every other row is unchanged (a scripted comparison: 14 keys moved, all of them this stage's, each to absent). At PR-open time no other open PR touches the file.service-automationplugin-auditaudit-writers.ts, excluded)9b7a0ef3)#21462(7e7e64b1)pnpm check:doc-authoringat the head: "sibling-package prose ids hold the baseline — 118 pinned site(s) across 39 file(s), 86309 string(s) read in 1253 parsed source(s), no growth, no burn-down unrecorded". No gate is added or loosened;scripts/check-doc-authoring.mjsis untouched.Changeset
.changeset/20751-services-strings-stage4-state-the-decision.md:patchfor@objectstack/service-automationand@objectstack/plugin-audit. Measured after building: the new sentences are in each package's built output (service-automationdist/index.jsanddist/index.cjs;plugin-auditdist/index.jsanddist/index.mjs). A TypeScript scan of every string literal and template text in the built output finds 0 tracker ids inservice-automation, and 2 inplugin-audit, both the excludedaudit-writers.tsoccurrence (once per build format). Control: the same scan reads 86 inplugin-security's built output.Text-only proof
A TypeScript-AST skeleton of each changed non-test
.tsfile, 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.9b7a0ef3against the head: 10 of 10 SAME (the two merges fromorigin/maintouched neither package). Controls on scratch copies ofplugin.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 template into two concatenated pieces reads SAME.Pins
Assertions that found a message by its tracker number now find it by what it says. No
codeorstatusassertion was touched; none of these strings is a coded refusal.builtin/config-unknown-keys.test.ts:97: thevisibleIfprescription by "blocks the screen from ever being submitted" instead of the id.builtin/decision-branch-routing.test.ts:210: the unclaimed-branch warning by "The branch selection is IGNORED" instead of the id.connector-degrade-cause.test.ts:185and:274: the degrade error by "retrying with backoff, attempt 1." and the husk warning by "stays absent from the connector registry until a retry succeeds"; the three hand-built fixture messages in the same file (:376,:396,:407), which mirror the production text, drop the id too.connector-descriptor-audit.test.ts:134: the inert-connector warning by "catalog descriptors (descriptor-only contract)".degraded-register-cause.test.ts:212: the degraded-registration warning by "the materializer retries with backoff"; its fixture message (:407) drops the id.plugin-auditobjects/sys-activity-type-open-vocabulary.test.tsasserts thetypehelp by three markers (built-in,open vocabulary,ADR-0052), all kept; no edit needed.The five edited test files were run with the verbose reporter (5 files, 50 tests passed) and every re-pinned case is listed as passed.
Tests
All through
scripts/pm/os-verify-lock.sh, every verdictVERDICT command-exit 0, at the headf300a255:turbo run build --filter=./packages/* --filter=./packages/*/*(71/71).@objectstack/service-automation: 166 files, 2053 tests passed;@objectstack/plugin-audit: 38 files, 618 tests passed.typecheckfor both, includingcheck:test-typecheck: OKfor each.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) atf300a255(24 paths vs merge base7e7e64b1, 195 changed lines): 74 commands, run one at a time from the worktree, 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 at1af6bed2, before the last merge.check:i18n: "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)";check:i18n-stale-fill: "OK (10 bundle set(s) — no new stale fills, 0 baselined)";check-issue-citations: "no issue citations added against 7e7e64b (17 file(s) read)";check:nul-bytes: OK, 9807 files;check:type-check-debt: "none above its recorded number";check:dual-build-cjs-loads: 105 require entry points across 66 packages load;check:dts-closure: 71 built packages, 167/167;check:sourcemap-no-sources-content: 68 packages, 524 maps;check:published-files: 69 publishable packages.f300a255: 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) and the artifact-roster families whose roster sits under one of this PR's directories (check-changeset-fixed,check-published-list-mirrorsand its self-test,check:authz-resolver,check:console-injection,check:error-code-casing,check:filter-alias-parity,check:published-readme-exports,check-dts-references --self-test;check:engine-double-contractandcheck:i18n-stale-fillare in the derived set). The path-scheduled CI jobs and the type-check lanes are CI's.pnpm lint(eslint . --no-inline-config, repo-wide, not narrowed) atf300a255: exit 0.origin/mainmoved by two commits after the ledger recompute (49524f69,packages/rest;aa463223,packages/specand object-grid surfaces). Neither touches the ledger,service-automationorplugin-audit; the PR's CI merge ref and the queue carry them.Acceptance notes
Noted, not filed:
packages/runtime/src/domains/automation-put-post-error-parity.test.ts(lines 57-70, assertion at line 247) andautomation-register-error-class.test.ts(lines 87-100, assertion at line 308) each rebuild the undeclared-config-key refusal in a test fake, copied fromengine.tswith both old ids, and asserttoContainof the 4277 id on their own fake's message. They stay green (the fake is self-consistent) but no longer mirror the engine's text.packages/runtimeis outside this stage's surface. Carrier: none.plugin-audit/src/audit-writers.tskeeps its one ledgered occurrence (excluded file, open PR fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, never evaluated (#21207) #21436); it is left for a later stage.plugin-audittest files carry the 11507 id in failure-message text; test files are outside the ledger.Generated by Claude Code