Repository navigation
docs(changeset): correct two scope sentences in the pending service-analytics masked-field note - #20991
docs(changeset): correct two scope sentences in the pending service-analytics masked-field note#20991objectstack-fleet[bot] wants to merge 1 commit into
Conversation
…rvice-analytics masked-field note The BREAKING banner named only a SQL deployment, while the SQL echo (POST /api/v1/analytics/sql) now refuses the same members whichever strategy serves the cube: its admission runs in callCtx, ahead of the strategy, and the ObjectQL strategy's echo renders without the engine. "What is not affected" read as unconditional, while the plugin bridge's fallback (a security service that predates getQueryableFields, or answers "no answer") refuses every field declaring a maskingRule whoever the caller is, a system context and a capability holder included. The list now carries that condition and points at the New hook paragraph. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #20991 (Part of #20935; a one-file follow-up to PR #20955, merged as ① Derived judgmentsAccept-set and public-surface changes the diff implies. None. The diff edits two sentences of a PENDING release note and no code, test, schema or generated artifact; no accept set moves and no public surface is added, removed or renamed by this PR. The behaviour the note describes landed on The two corrected sentences, each tested against the code on
Nothing else in the note changed. The diff's two hunks touch line 11 and lines 23-25 only. Byte-identical before and after: the frontmatter ( Author-shown and AI-facing text, sentence by sentence (only what is false, unsourced, imprecise or over-broad is listed; everything else in the PR body, the commit message and the two corrected sentences tested true against the tree):
② Semver level
③ Boundary flags
Check-runs on Implemented-by: VERDICT: PASS Adopted and posted by
Generated by Claude Code |
|
os-decision-facets 决策请求:确认更正一条尚未发版的安全发布说明(PR #20991)· 2026-10-01T00:59Z
一句话问题: p0 安全修复 #20935 已合并,它附带的发布说明还没发版。说明里有两句写得不准,会让升级的客户误判自己受不受影响。仓库检查规定:改一条待发的发布说明,要由人确认( 背景与前提(每条可复核):
选项 × 代价:
业务含义: A 等于发布说明和产品实际行为一致;B 等于发版说明里留下一句与行为不符的安全说明。 四棱:
Prior rulings read: 推荐:A。 只看①选 A;②③④ 是否翻转:否。回退项:B。置信缺口:两句的正确性来自代码阅读加已有测试,未在运行中的服务上实测。 裁后执行: 维护者答 A 后,席位把本 PR 转为 ready 并放入合并队列(Check Changeset 按设计保持红,本评论即其记录);合并后收尾。答 B 则关掉本 PR。 你要做的: 回一个字母,A 或 B。 Generated by Claude Code |
The maintainer's answer: B, do not correct the note; this PR is closed unmerged
Generated by Claude Code |
… to the SQL echo on either strategy, and anchor the nested-relation note to its measured base (objectstack-ai#21012) Part of objectstack-ai#20917 Clause-②: no Part of objectstack-ai#20933 · Part of objectstack-ai#20887 A follow-up on three landed cards. It corrects one sentence in each of three release notes of `@objectstack/service-analytics` that are still pending on `main`, before release PR objectstack-ai#20639 consumes them. The order is this lane's dispatch note on objectstack-ai#20917. The reasons are the `domain:spec` seat's pointer `5922364600` on objectstack-ai#6021 (two banners) and the named follow-up in section ② of the delta review `5922352898` on PR objectstack-ai#20916 (one paragraph). PR objectstack-ai#20991 is the precedent: it makes the same banner correction to objectstack-ai#20935's note, and that note is not touched here. No code, test or other file changes. ## The three sentences | Note | Before | After | |:---|:---|:---| | `.changeset/20917-analytics-field-permission-gate.md`, the BREAKING banner | "BREAKING for analytics queries on a SQL deployment that read a field the caller may not read." | "BREAKING for analytics queries that read a field the caller may not read: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube." | | `.changeset/20933-analytics-relationship-path-admission.md`, the BREAKING banner | "BREAKING for analytics queries on a SQL deployment that read a related object through a relationship path the cube does not declare." | "BREAKING for analytics queries that read a related object through a relationship path the cube does not declare: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube." | | `.changeset/20887-analytics-nested-relation-engine-answer.md`, the first sentence of **Why** | "Measured on the base over one fixture with the real security layer (…)." | "Measured on the base before the field-level gate (objectstack-ai#20917) and the relationship-path admission (objectstack-ai#20933) landed, over one fixture with the real security layer (…)." | The parenthesis in the third row is unchanged and elided here. Both banners keep their bold. Everything else in the three files is byte-identical: front matter, summary line, `Clause-②` line, ADR-0087 disposition marker and every other paragraph. Each file's diff is one line (+1/−1). ## The readings behind each sentence ### Banner of the objectstack-ai#20917 note **Before, by source, at `95555e71` (the parent of objectstack-ai#20931's landing `1571aedc`):** - `packages/services/service-analytics/src/analytics-service.ts:2305-2333`: `generateSql()` runs `callCtx` (2329), resolves a strategy (2330) and returns that strategy's `generateSql` (2333). - `analytics-service.ts:1283-1327`: `callCtx` runs the object-level admission (1308) and the read-scope pre-pass. It has no field-level gate. No non-test source in the package names `getReadableFields` at that commit (`git grep` exit 1). The control is the landing `1571aedc`, where it is named in three files. - `strategies/objectql-strategy.ts:372-638`: the ObjectQL strategy's `generateSql` renders the statement from the cube definition and returns it (638). It makes no engine call, so it never reaches the engine's field guards. That strategy's `execute()` reaches `engine.aggregate`, which is why the query door already refused on it. - So for a field the caller may not read, the echo printed a statement on the ObjectQL strategy as well. **After, on `main` at `a5bce40888`:** - `analytics-service.ts:1453-1515`: `callCtx` runs the field-level gate (1487) after the object admission (1481). - `generateSql()` calls `callCtx` before it resolves a strategy (2580). ### Banner of the objectstack-ai#20933 note **Before, by source, at `83480c6a` (the parent of objectstack-ai#20962's landing `5f6b63a6`):** - `analytics-service.ts:1452-1507`: `callCtx` admits objects over `queryObjects` (1477). That set is `cubeObjects` (1584-1588, 1596-1608): the base object and the declared joins only. An object reached through an undeclared relationship path is not in it. - The field-level gate (1483) asks the security service's `getReadableFields`. That answer is field-level only and never asks about object-level read (`packages/plugins/plugin-security/src/security-plugin.ts:5410-5412`, `computeReadableFields` 5440-5461 over `resolveProjectionFieldMask` 5517 ff.). Object-level read is the separate `canReadObject` (5641). So the field gate does not refuse a readable field on an unreadable related object. - `objectql-strategy.ts` is byte-identical at `95555e71`, `83480c6a` and `5f6b63a6` (`git diff --quiet`, exit 0 for both ranges). - Its `generateSql` passes a one-hop cross-object dimension through `planCrossObject` (895 ff.). That function throws only for the out-of-envelope shapes: a cross-object time dimension, measure or filter, a multi-hop dimension, a non-recombinable measure. The one-hop dimension renders a LEFT JOIN (461-466), and the statement is returned (638). - So on the ObjectQL strategy the echo printed a statement for a one-hop dimension through a related object the caller may not read. **After, on `main` at `a5bce40888`:** - `queryObjects` (1607-1616) adds every object a named member reads. - The admission at 1481 covers that set before `generateSql()` resolves a strategy (2580). ### Measured after, on both strategies The measurement was a scratch probe at `a5bce40888`, copied into `packages/rest/src` for one run and removed by an EXIT trap. It was never committed; `git status --porcelain` was empty afterwards. - **Build:** the probe's dependency closure was built under `os-verify-lock.sh` first: `turbo run build --concurrency=1` over `@objectstack/service-analytics...`, `@objectstack/plugin-security...`, `@objectstack/objectql...` and `@objectstack/driver-sql...`. That is 19 tasks, `VERDICT command-exit 0`. - **Run:** `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2` on the one file gave 1 file / 2 tests passed, `VERDICT command-exit 0`. - **Fixture:** the composition is the shipped one, with the real security layer: `SecurityPlugin` over `ObjectQL` on `SqlDriver` (SQLite), and `AnalyticsServicePlugin` over the same engine. There are two compositions: `native` (the plugin's own capabilities) and `objectql` (narrowed to the engine-aggregate path). - **What was asked:** each query body first passed the door's own body schema (`AnalyticsQueryRequestSchema`). It then went to `generateSql`, the call `POST /api/v1/analytics/sql` makes (`packages/runtime/src/domains/analytics.ts:141-159`). The thrown `status` is the HTTP status (`packages/runtime/src/dispatcher-plugin.ts:646-649`). | Question, inferred cube | Member caller, `native` | Member caller, `objectql` | System caller, either | |:---|:---|:---|:---| | A field the member may not read, grouped | `403 PERMISSION_DENIED` | `403 PERMISSION_DENIED` | statement printed, by the composition's strategy | | The same field, filtered | `403 PERMISSION_DENIED` | `403 PERMISSION_DENIED` | statement printed | | A one-hop dimension through a related object the member may not read, path not declared | `403 PERMISSION_DENIED` | `403 PERMISSION_DENIED` | statement printed | | Control: a readable field | statement printed (`NativeSQLStrategy`) | statement printed (`ObjectQLStrategy`) | statement printed | | Control: a readable related object, path not declared | statement printed (`NativeSQLStrategy`) | statement printed (`ObjectQLStrategy`) | statement printed | Before and after both hold for both banners. So each banner is scoped the way PR objectstack-ai#20991 scoped objectstack-ai#20935's. The note's own pins in `packages/rest/src/analytics-field-permission-gate.test.ts` and `analytics-relationship-path-admission.test.ts` assert the same echo refusals on both compositions. The probe adds the system-caller and readable controls, and it ran the bodies through the door's schema. ### The objectstack-ai#20887 **Why** paragraph The dev measured on the branch point of PR objectstack-ai#20916, `00a92e18da`. That is the parent of its first commit, `5687276296`. **The base predates both gates.** `git merge-base --is-ancestor 00a92e1 X` exits 0 for X = `95555e71`, `1571aedc`, `83480c6a` and `5f6b63a6`, so both landings descend from it. The reverse, `1571aedc` against `00a92e18da`, exits 1. Its control leg, `00a92e18da~200` against `00a92e18da`, exits 0, and the repository is not shallow. **Clause one, "answered rows for a condition on a field the caller cannot read", held on that base:** - No non-test source in the package names `getReadableFields` (`git grep` exit 1; control: six hits in `analytics-service.ts` on `main`). - `callCtx` (`analytics-service.ts:1283` ff. at `00a92e18da`) runs only the object admission (1308) and the read-scope pre-pass. - The base filter normalizer flattened the form to a dotted member (`strategies/filter-normalizer.ts:1296-1299`). The native strategy joined the declared include. - The dev's first report (`5916988260` on objectstack-ai#20887) records the measured rows. **Clause two, "without the declared join it named a table that does not exist (500)", held on that base:** - `strategies/native-sql-strategy.ts:771-783` falls back to the relationship name as the joined table when no join is declared. - The join allowlist applies to a compiled dataset only. An inferred cube has none (`analytics-service.ts:1266-1268`; `native-sql-strategy.ts:575-576`). - The object admission covered the base object and the declared joins only (`analytics-service.ts:1404`, `1416` ff.). So nothing refused before the statement ran. The paragraph is anchored to that base in the delta review's own words; the clauses are kept. ### The ADR-0087 gate's reading `breakingDeclaration` and `readDisposition` were re-run on each note at `a5bce40888` and at this head. All three notes read the same on both: - `breaking: true`; - the signals `BREAKING`, `bang` and `clause-②-narrowing`; - the disposition `not-required (no-migration-prescription)`. The gate itself counts the three as inherited, not introduced: it skips a changeset already breaking at the branch point. ## Changeset gate: no `skip-changeset`, and `Check Changeset` stays red This PR edits three pending changesets and adds none, so `Check Changeset` goes red by design. `check-empty-changeset.mjs --base origin/main` exits 1 and refuses all three files as the DELIBERATE CORRECTION class. Ruling D on objectstack-ai#18375 says `skip-changeset` is never applied to a PR that edits an existing changeset, so no label is applied. `Check Changeset` is not a required context. **Confirmation:** this lane's at-tier contract review record on this PR's head judges each rewritten sentence above. The sentences are: 1. the objectstack-ai#20917 note's banner, now scoped to include `POST /api/v1/analytics/sql` on either strategy; 2. the objectstack-ai#20933 note's banner, scoped the same way; 3. the first sentence of the objectstack-ai#20887 note's **Why**, now anchored to the base before the field-level gate and the relationship-path admission landed. `Clause-②: no` was checked against `scripts/pm/clause2-line.mjs`. A wording edit to pending notes widens no accept set and adds no public surface, so the value is `no`. With no arm, the line declares no direction. ## Verification at `56fc0e77e3` `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (exit 0) derived 19 commands from the three changed paths. Each ran on this head, with its exit code captured before any pipe. - **18 exit 0:** - `check-adr-0087-registration.mjs --base origin/main` and `--self-test` - `check-changeset-no-major.mjs --base origin/main` and `--self-test` - `check-closing-keyword-parity.mjs` and `--self-test` - `check-comment-mask-corpus.mjs` - `check-empty-changeset.mjs --self-test` - `pm/release-rehearsal-clone.mjs --self-test` - `check:changeset-gate-self-tests`, `check:driver-memory-census`, `check:gitlink-declared`, `check:nul-bytes`, `check:objectui-changeset`, `check:pm-changeset-deadline-census`, `check:published-files`, `check:refd-timer-probe`, `check:watch-hint-literal` - **1 exit 1, by design:** `check-empty-changeset.mjs --base origin/main`, the DELIBERATE CORRECTION class above. - **Roster family:** `check-changeset-fixed.mjs` (its roster lives under `.changeset/`) exited 0. - **Reconciliation:** `dispatch-gates.mjs --ran` with the recorded exit codes exited 0. It read "19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN". - **Main moved:** `origin/main` gained `f8178ffece` after the branch point. It touches none of the three notes, and all three are still present there. - **NOT MEASURED, by design:** the type-check lanes and the package test suites. The diff touches no TypeScript and no package source. ## Acceptance notes - **Wording noted, not changed (corrections only, no new claims).** - The objectstack-ai#20917 note's **What changed** ends: "the ObjectQL strategy and the data API already refused them". Its ADR-0087 marker says the engine "already refuses" such queries "on the ObjectQL strategy". Both hold for the query and dataset doors, not for the SQL echo, which printed on that strategy (reading above). The corrected banner carries the echo's scope. - The objectstack-ai#20933 note's **Refusals that change form** says: "On the ObjectQL strategy a related object the caller may not read was already refused". That holds for the query door. On the echo, a one-hop dimension through such an object printed (reading above). The corrected banner carries the echo's scope. - **Where the probe measured.** It measured the service call the door relays, not the mounted HTTP route: identity resolution needs an auth composition the probe did not boot. The relay and the status mapping are cited at file:line above. - **Not in this PR:** objectstack-ai#20935's note. PR objectstack-ai#20991 holds it. - **Disclosure discipline holds.** No request recipe, field spelling or returned value appears here. ## Patch rounds (the seat's append from the dev's report on objectstack-ai#20917; the dev writes a body only once) ### Patch round 1 At `db64c37690`, on the seat's note `5923016038` on objectstack-ai#20917, which takes the dev's class (a) finding into this PR. Two sentences beside the corrected banners are qualified to the doors they hold for. Nothing else changes: each is still one sentence, rewrapped in place, and both ADR-0087 disposition markers are byte-identical. | Note | Before | After | |:---|:---|:---| | `.changeset/20917-analytics-field-permission-gate.md`, **What changed**, last sentence | "The native-SQL strategy, the one a SQL driver serves first, answered such queries; the ObjectQL strategy and the data API already refused them." | "The native-SQL strategy, the one a SQL driver serves first, answered such queries; the ObjectQL strategy already refused them on `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query`, as the data API did, but printed the statement on `POST /api/v1/analytics/sql`." | | `.changeset/20933-analytics-relationship-path-admission.md`, **Refusals that change form**, first sentence | "On the ObjectQL strategy a related object the caller may not read was already refused; it now answers the analytics door's refusal rather than the engine's, the same one a declared join gets." | "On the ObjectQL strategy a related object the caller may not read was already refused on `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query`, though `POST /api/v1/analytics/sql` printed the statement; on those two doors it now answers the analytics door's refusal rather than the engine's, the same one a declared join gets." | **Readings:** - **objectstack-ai#20917's sentence, at `95555e71`.** On the query door, the ObjectQL strategy's `execute()` reaches the engine (`objectql-strategy.ts:301`) before it renders its own echo (350-354). The dataset door runs the same `execute()`, so both refused through the engine. The SQL echo's `generateSql` (372-638) renders with no engine call, and `callCtx` had no field gate (`analytics-service.ts:1283-1327`). So `POST /api/v1/analytics/sql` printed the statement. - **objectstack-ai#20933's sentence, at `83480c6a`.** On the query and dataset doors, a one-hop dimension through a related object goes through `executeCrossObject`. Its `resolveFkAttr` (1176 ff.) reads that object through the engine as the caller (1211-1215). The echo renders the join (461-466) and returns the statement (638), with no engine call, and the object was outside the admitted set (`analytics-service.ts:1584-1608`). After the change, on `main`, the echo answers `403 PERMISSION_DENIED` on both strategies (round 0's probe). **ADR-0087 reading at `db64c37690`:** for both notes, `breakingDeclaration` reads `breaking: true` with the signals `BREAKING`, `bang` and `clause-②-narrowing`, and `readDisposition` reads `not-required (no-migration-prescription)`. Both are unchanged from `a5bce40888`, and the objectstack-ai#20887 note reads the same. **Gates at `db64c37690`:** - `dispatch-gates.mjs --commands` derived the same 19 commands. 18 exited 0. `check-empty-changeset.mjs --base origin/main` exited 1 by design: the DELIBERATE CORRECTION class, with all three notes named. - The roster gate `check-changeset-fixed.mjs` exited 0. - `--ran`: 19 derived, 19 run, 0 not measured. - The derivation was repeated at `origin/main` `8055ff2279`, which had changed one roster file, and gave the same 19 commands. None of the three notes changed on `main` over that range. **Acceptance note, wording kept:** both ADR-0087 disposition markers are HTML comments, and they stay byte-identical. The objectstack-ai#20917 marker still says the engine "already refuses" such queries "on the ObjectQL strategy". Like the two sentences above, that wording holds for the query and dataset doors, not for the SQL echo. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…the error-code ledger lists its two refuse() codes (objectstack-ai#21106) (objectstack-ai#21150) Fixes objectstack-ai#21106 Clause-②: yes Three release-text follow-ups from the at-tier contract review of PR objectstack-ai#21084 (comment `5926057594`), all due before Version Packages objectstack-ai#20639 next picks up PR objectstack-ai#21084. Each sentence was checked against the code at PR objectstack-ai#21084's merge commit `8368f1c0`. The target files are byte-identical between `8368f1c0` and this branch's base (`git diff --stat` printed nothing). No package source changes. 1. **A deliberate correction of the stage-① pending note.** `.changeset/20281-connector-sync-moved-to-mapping.md` said `connectorSource` was "declared, not yet executed" and that "Nothing executes it in this release". The stage-② note `.changeset/20919-spec-connector-source-live.md` says the executor reads it. Both compile into the same `@objectstack/spec` CHANGELOG version. 2. **The "provenance, not identity" ledger sentence** for this release's `ERROR_CODE_LEDGER` face changes, in a new spec note, `.changeset/21106-error-code-ledger-provenance-rows.md`. 3. **The two `@objectstack/service-automation` provenance rows**, `MAPPING_NOT_FOUND` and `UNSUPPORTED_TRANSFORM`, in `packages/spec/src/api/error-code-ledger.zod.ts`. A hand pin in the provenance gate's own test, `packages/spec/scripts/check-error-code-provenance.test.ts`, guards them. ## 1 · The stage-① note correction | | Before | After | |:---|:---|:---| | Lead label | "**Added (declared, not yet executed):**" | "**Added:**" | | Last sentence of that paragraph | "Nothing executes it in this release, and `os validate` / `os build` warn when it is authored." | "The connector sync executor, `@objectstack/service-automation`'s `pullConnectorSource` (objectstack-ai#20919), reads it; nothing schedules a pull until the `job` stage lands." | Everything else in the file is byte-identical: the front matter, the summary line, the BREAKING banner, the FROM → TO table and the retirement kit. The diff is +4 / −3 lines in one paragraph. **Readings at `8368f1c0`:** - **"reads it".** `packages/services/service-automation/src/connector-pull.ts` `pullConnectorSource` (:209) reads the mapping through `getMetaItem` and its `connectorSource`. It makes one action call and writes through `runImport`. The liveness rows say the same: `packages/spec/liveness/mapping.json`, `connectorSource` `live`, evidence `connector-pull.ts#pullConnectorSource`. - **"nothing schedules a pull until the `job` stage lands".** Outside tests, `pullConnectorSource` has three places in its own package: the definition, the index re-export and the plugin method (`plugin.ts:621`). No package source calls the plugin method. - **Why the review's suggested clause "`os validate` / `os build` warn when it is authored" is NOT kept.** It is false at `8368f1c0`, measured: - The ledger row is `live` with `authorWarn: true`. `lintLivenessProperties`'s `describe()` (`packages/lint/src/lint-liveness-properties.ts`) has no `live` branch. It throws its sentinel for that pair, by design: its own header calls the pair "a ledger authoring mistake". - Through lint's source, against the built spec: `runAuthoringRules('validate', …)`, the call `os validate` makes (`packages/cli/src/commands/validate.ts:522`), THREW `lintLivenessProperties: ledger entry has unrecognised status "live"` for a stack whose `mappings[]` carries `connectorSource`. The control without `connectorSource` gave 0 findings. - The runtime metadata-write door (`runRuntimeAuthoringRules`) answered one `authoring-rule-threw` advisory in place of the liveness warning. - Read at `99398542b` and again at this head. `lint-liveness-properties.ts`, `authoring-rules.ts`, `runtime-gate.ts` and `liveness/mapping.json` have no diff between `8368f1c0` and `99398542b`. The CLI process itself was not run: its closure was not built here. - So the corrected note claims no warning. The defect is reported to the seat as a separate finding below and not changed here. ## 2 · The ledger sentence, and the counts behind it Measured on `ERROR_CODE_LEDGER` at `8368f1c0` against its parent `2742e537`, comments stripped: | Key | Parent | `8368f1c0` | Change | |:---|---:|---:|:---| | `@objectstack/rest` | 83 | 76 | −7: `AMBIGUOUS_MATCH`, `BLANK_MATCH_KEY`, `CONCURRENT_UPDATE`, `ERR_DATASOURCE_UNAVAILABLE`, `NO_MATCH`, `SUMMARY_RECOMPUTE_FAILED`, `UNIQUE_VIOLATION` | | `@objectstack/core` | 12 | 17 | +5: `AMBIGUOUS_MATCH`, `BLANK_MATCH_KEY`, `NO_MATCH`, `SUMMARY_RECOMPUTE_FAILED`, `UNSUPPORTED_TRANSFORM` | | `@objectstack/types` | absent | 3 | new key: `CONCURRENT_UPDATE`, `ERR_DATASOURCE_UNAVAILABLE`, `UNIQUE_VIOLATION` | | owner keys | 30 | 31 | | | union (`ErrorCode`) | 280 | 280 | none added, none removed | **The card and the review say rest lost −8. The measurement is −7.** Eight codes moved (5 to core and 3 to types), but rest kept `UNSUPPORTED_TRANSFORM`, which `resolveNamedMapping` still stamps. Literal counts in `packages/rest/src` non-test source at `8368f1c0`: 0 for each of the seven, and 1 for `UNSUPPORTED_TRANSFORM`. The note says seven. The sentence follows the objectstack-ai#20206 (`5f9d7d78`) and objectstack-ai#19441 (`3f9e2eaa`) paragraphs. It names both steps of this release's face change, the objectstack-ai#20919 move and this PR's two rows, and says the union, the wire and the HTTP answers are unchanged. ## 3 · The two rows, and why a pin rather than a wider gate - **Stamp sites at `8368f1c0`** (unchanged since). `connector-pull.ts:234` `refuse('MAPPING_NOT_FOUND', 404, 'mapping_not_found', …)` and `:303-304` `refuse('UNSUPPORTED_TRANSFORM', 400, 'unsupported_transform', …)`. Both go onto `ConnectorPullError.code`, and the class is exported from the package index. - **Both codes are registered extension codes.** They are listed under `@objectstack/rest`, and `UNSUPPORTED_TRANSFORM` under `@objectstack/core` too. Neither is in `errors.zod.ts`. The executor's other five codes are standard-catalog members and owe no row: `VALIDATION_ERROR`, `EXTERNAL_SERVICE_ERROR`, `INTEGRATION_ERROR`, `SERVICE_UNAVAILABLE`, `INVALID_FIELD`. - **Placement.** The rows go in the key's existing ASCII order: `MAPPING_NOT_FOUND` after `INVALID_SIGNAL`, and `UNSUPPORTED_TRANSFORM` last. One comment records the stamp sites, the statuses and the reachability reading: no HTTP door on this tree, so the thrown value is the boundary. No other package's rows were touched. - **Not widened.** `check:error-code-provenance`'s header declares it blind to a helper indirection (a `makeError(code, …)` call site). It also says "Widening is a gate-population change with an unmeasured blast radius — its own card, never a rider". So this PR keeps the gate's patterns and pins the two rows by hand instead. - **Measured for the seat, read-only.** The `refuse('CODE', …)` call-site form stamps a registered code at 7 sites in 3 packages: core `artifact-packages.ts` ×4, runtime `artifact-collections.ts` ×1, and these two. Before this PR, these two were the only unlisted ones; after it, none is unlisted. - **The pin.** A new block in the gate's test file has two halves. One pins the blind spot itself: `scanSourceText` finds no site in a `refuse('X', …)` call. The other asserts that `ERROR_CODE_LEDGER['@objectstack/service-automation']` lists each code. It reads the ledger module inside `packages/spec`, so the suite still reads nothing outside its package. - **Ablation, two legs** through `scripts/ablation-replace.mjs` in WRAP mode, run from the committed state at `c227eb366`: - Removing the `MAPPING_NOT_FOUND` row gave `1 failed | 16 passed (17)`: "expected [ …(9) ] to include 'MAPPING_NOT_FOUND'". - Removing the `UNSUPPORTED_TRANSFORM` row gave `1 failed | 16 passed (17)`, on that code's case. - Each leg restored the file (blob `229345964e13` = HEAD, `git diff HEAD` empty). - A first attempt at leg one was refused by the tool before any test ran: its replacement text already existed in the file. The anchor was changed and the leg re-run. That first attempt is not counted as a run. ## Changeset gate: no `skip-changeset`, and `Check Changeset` stays red by design This PR edits a pending changeset that it did not add. `node scripts/check-empty-changeset.mjs --base origin/main` exits 1 and refuses `.changeset/20281-connector-sync-moved-to-mapping.md` as the DELIBERATE CORRECTION class. The precedents are PR objectstack-ai#20991 and PR objectstack-ai#21012. Ruling D on objectstack-ai#18375 says `skip-changeset` is never applied to a PR that edits an existing changeset, so no label is applied. `Check Changeset` is not a required context. **Confirmation requested in writing on this PR:** the stage-① note's `connectorSource` paragraph now says the executor reads the binding and nothing schedules a pull yet. It no longer says nothing executes it or that `os validate` / `os build` warn. ## `Clause-②: yes`, not the claim's `no` The claim (`5926939917`) and the dispatch say `patch` with `Clause-②: no`. The dispatch also says not to keep `no` silently if the diff widens a public surface, and it does: - Two literal members are added to `ERROR_CODE_LEDGER['@objectstack/service-automation']`, a published `as const` face. - The ledger header (the objectstack-ai#16404 ruling) names this ledger, together with `StandardErrorCode`, as the published contract face for error codes. - The two precedents this card names, objectstack-ai#20206 and objectstack-ai#19441, each declared a provenance-row addition as `@objectstack/spec` `minor` with `Clause-②: yes`. Both say "What widens is the per-package face". So the new note is `minor` with `Clause-②: yes`, and this body's second line matches it. No accept set changes: the `ErrorCode` union is unchanged. Every package is in the one `fixed` group, and `@objectstack/spec` already has a pending `minor`, so the released version is the same either way. The seat can flip it back if it reads the precedent differently. ## Verification at `5748c8ddd` (base `99398542b`, merged with `origin/main` `39ab2940e`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (exit 0) derived 87 commands from the four changed paths. Each ran on this head, with its exit code captured before any pipe: - **84 exit 0.** Among them: `check:error-code-provenance` ("scanned 2636 files; 335 registered-code stamp site(s): 316 listed, 19 waived" / "OK"), `check-adr-0087-registration.mjs --base origin/main` ("this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen)"), `check-changeset-no-major.mjs --base origin/main` ("This diff introduces no `major` bump"), `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:liveness`, `check:error-code-casing`, `check:dispatcher-error-vocabulary`, `check:cross-package-test-inputs`, `check:nul-bytes`, `check:query-options-erasure` and `check:type-check-debt`. - **1 exit 1, by design:** `check-empty-changeset.mjs --base origin/main`, the DELIBERATE CORRECTION class above. - **2 NOT MEASURED** (exit 3, `PREREQUISITE NOT MET`): `check:dual-build-cjs-loads` (needs a full `pnpm build`) and `check:lean-entry-closure` (needs `@objectstack/objectql` built). CI builds the tree. Reason: a built tree was not produced here. - Reconciled: `dispatch-gates.mjs --ran` exit 0, "87 derived famil(ies) accounted for — 85 run, 2 NOT-MEASURED". - `pnpm --filter @objectstack/spec build`: `VERDICT command-exit 0`. Then `pnpm --filter @objectstack/spec check:generated`: "All 15 generated artifacts are up to date". - `vitest run --project local` on `src/api` plus `scripts/check-error-code-provenance.test.ts`: 47 files / 1546 tests passed. The 12 other spec test files that read the ledger: 182 tests passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0. That covers `tsc`, `check:scripts-typecheck`, and `check:test-typecheck` ("52 file(s) / 246 error(s) / 135 pinned signature(s) held", unchanged). - Main moved during the run: `origin/main` gained 13 commits after the branch point. None touches the four files. The delta against `39ab2940e` is exactly them (+70 / −4). ## Acceptance notes - **Read and kept in the stage-① note:** - "Runtime behaviour is deliberately **unchanged**: no connector sync ever ran." This is about the retired connector-attached keys (`syncConfig` / `fieldMappings`), and it stays true of them. - "It carries no cadence (a `job` sets that)." This describes the design; the corrected clause says nothing schedules a pull until the `job` stage lands. - **Reported to the seat, not changed here:** - **The liveness lint throws on `connectorSource`.** The `live` + `authorWarn` row reaches `describe()`'s sentinel throw, so `os validate` / `os build` stop with an internal lint error, and the runtime door answers `authoring-rule-threw`, instead of warning on an authored `connectorSource`. The comments in `authoring-rules.ts` and `runtime-gate.inert-type-writes.test.ts` still say it warns. The fix is a choice for its own card: teach `describe()` a caveat branch for `live`, or change the row. - **`turbo` 2.11.5 edits `AGENTS.md`.** It arrived with the development-dependencies bump (`840ec9dab`, now on `main`). On every turbo invocation it sees as an AI agent's, it appends a managed `turborepo-agent-rules` block to `AGENTS.md`, a Tier H governed surface, and `turbo.json` declares no `agentGuidance: false`. Measured here: `pnpm exec turbo run build --filter='@objectstack/lint...'` and `pnpm check:type-check-debt` each left `M AGENTS.md`, +11 lines. Each time it was restored with `git checkout HEAD -- AGENTS.md` (blob `e9e211fc` = HEAD) and never committed. An agent that commits with `-a` would carry it into its PR. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20935
Clause-②: no
A one-file follow-up to PR #20955, which landed as
83480c6a2f. It corrects two sentences in the release note.changeset/20935-analytics-masked-field-not-queryable.md, which is still pending onmain. The order is the dispatch note5922157430on #20935, and the "Changeset wording" bullet of the review adoption5921385686on PR #20955. Release PR #20639 consumes pending changesets when it merges, so this needs to land first. No code, test or other file changes.What changed under the note
1. The BREAKING banner was too narrow.
Before:
After:
Why, read on
05be352596:AnalyticsService.generateSql()(the SQL echo) runscallCtxbefore it picks a strategy. That is the same object- and field-level admissionquery()runs.generateSqlrenders the statement itself and never reaches the engine's query guards.403on both strategies./analytics/queryand the dataset door, the ObjectQL strategy already refused these queries, so "on a SQL deployment" stays right for those two.2. "What is not affected" was too broad.
Before:
After:
Why:
getQueryableFieldsbridge inpackages/services/service-analytics/src/plugin.tshas a fallback. It applies when the service lacks the method or answersundefined.getReadableFieldsanswer and removes every field whose declaration carries amaskingRule. It reads nothing about the caller.getReadableFieldsanswers the full field set for a system context (security-plugin.ts). So under the fallback a system caller loses those fields too.field-read-admission.tsrefuses any named field that either answer leaves out.field-query-admission-gate.test.tspins this. Its case "fails CLOSED for a security service that predates getQueryableFields: every field declaring a masking rule is not queryable" also asserts the system caller is refused.When the service does answer, all three sentences hold as written:
getQueryableFieldsreturns the full set for a system context, and it keeps a field whose rule is lifted for the caller. The case "serves a caller the security service reports the rule lifted for" pins the second point.Not changed: the summary line, the changeset's own
Clause-②: yes (narrowing)line, the ADR-0087 disposition marker, the banner's BREAKING marker, and the "What changed", "New hook" and remedy paragraphs. Re-run on this head, the ADR-0087 gate'sbreakingDeclarationstill reads all three signals from the file:BREAKING,bangandclause-②-narrowing. Its disposition readsnot-required (no-migration-prescription).Scope
plugin.tssays the fallback "over-refuses a caller the rule is lifted for — a system one included". The fix stays in the same file and adds nothing new to verify. A qualifier on the system sentence alone would have implied that the capability holder is unaffected even under the fallback.Changeset gate: no
skip-changeset, andCheck Changesetstays redThis PR edits a pending changeset and adds none, so
Check Changesetgoes red by design:check-empty-changeset.mjsrefuses it as the DELIBERATE CORRECTION class.Ruling D on #18375 says
skip-changesetis never applied to a PR that edits an existing changeset, so no label is applied.Check Changesetis not a required context.Confirmation requested in writing on this PR: the note
.changeset/20935-analytics-masked-field-not-queryable.mdfrom PR #20955 is corrected in two places:POST /api/v1/analytics/sqlon either strategy.securityservice answersgetQueryableFields.Clause-②: nowas checked againstscripts/pm/clause2-line.mjs. The value answers whether the change widens an accept set or adds public surface, and a wording edit to a pending note does neither, so the value isno. With no arm, the line declares no direction.no (narrowing)would declare a breaking change, and this PR makes none.Verification at
fa6f9f7133node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(exit 0) derived 19 commands from the one changed path. Each ran on this head:check-adr-0087-registration.mjs --base origin/mainand--self-testcheck-changeset-no-major.mjs --base origin/mainand--self-testcheck-closing-keyword-parity.mjsand--self-testcheck-comment-mask-corpus.mjscheck-empty-changeset.mjs --self-testpm/release-rehearsal-clone.mjs --self-testcheck:changeset-gate-self-tests,check:driver-memory-census,check:gitlink-declared,check:nul-bytes,check:objectui-changeset,check:pm-changeset-deadline-census,check:published-files,check:refd-timer-probe,check:watch-hint-literalcheck-empty-changeset.mjs --base origin/mainrefuses with the DELIBERATE CORRECTION class described above.check-changeset-fixed.mjs(its roster lives under.changeset/) exited 0.dispatch-gates.mjs --ranwith the recorded exit codes exited 0, reporting "19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN".Acceptance notes
.changeset/20917-analytics-field-permission-gate.mdand.changeset/20933-analytics-relationship-path-admission.mdcarry the same "on a SQL deployment" banner. They are not edited here, per the order, and are reported to the seat. In both cases the gate the change added sits incallCtx, whichgenerateSql()shares. Before each change, that gate did not exist or did not cover the objects in question, and the ObjectQL echo never reached the engine. So the narrowing on the/analytics/sqlecho reaches both strategies there too.Generated by Claude Code