Skip to content

fix(scenarios): the loader refuses an operation its persona cannot reach, and two cards stop writing hidden fields - #30846

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-9464u3
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-9464u3

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:devx#3 · os-elon-musk · session_016WV4cyjYBCnWT8iGQXxoeH

Fixes #30802
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:场景卡片 loader 新增一条拒收规则:操作的执行 persona 看不到的对象、行或字段,卡片必须声明忠实构建会给出的那个拒绝;同时修好这条规则在 main 上拦下的全部 5 个操作:invoice、project 两张卡不再写 persona 看不见的字段;travel_expense 的「员工审批同事报销单」按裁决 A 改为「员工审批自己报销单的经理步骤」,测的是申请人永远不是审批人。README 示例改名 remove_ben。
  • 为什么改:卡片让 persona 写它看不见的字段、改它看不见的行,忠实的构建只会拒绝,评分就把正确的 app 判成失败。
  • 风险与回滚:只动私有测试包 @objectstack/scenarios 和 docs/qa/scenarios 下三张卡,不动 seed、问题和计算真值,不发布任何包;回滚即 revert。
  • 先看:packages/qa/scenarios/src/loader.ts 的 checkReach(规则本体);docs/qa/scenarios/travel_expense.json 的 employee_approves_own_claim(按裁决改的那一处)。
  • 两分钟验证:pnpm --filter @objectstack/scenarios test,预期 18 个测试文件、244 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

The loader refuses an operation its acting persona cannot reach (packages/qa/scenarios/src/loader.ts, checkReach). A persona is a requirement of the app (hidden = must not read, scope = sees only those rows), so such an operation has one outcome a faithful build gives, decided from the card alone, in the order the platform's gates run:

  1. the operation's object is hidden from the persona: the outcome must be refused with PERMISSION_DENIED;
  2. the target row is outside the persona's row scope: refused with RECORD_NOT_FOUND;
  3. a written field is hidden from the persona: refused with PERMISSION_DENIED.

The two codes are literals typed ErrorCode and parsed once at load (ADR-0112), so a code the spec's catalogue stops carrying fails the typecheck and every test run. They are literals, not ErrorCode.parse(...) results, because check:dispatcher-error-vocabulary reads a code constant only when it resolves to a literal. Path scopes resolve through the seed rows with the resolver the persona truths already use (inScope, which the persona-truth evaluation now shares). The rule was built and proven on claude/issue-30770-loader-reach-held @ 82b1acd8; this branch merges that sha and main, and adds the card fixes.

Card fixes. The invoice and project operations use the same move as PR #30801: drop the hidden field from the write, and keep the persona, the target and the outcome. The travel operation is retargeted as ruled:

Card · operation Before After
invoice_with_line_items · add_line sales_rep writes unit_price: 5, which it must not read; expected applied writes { invoice, product, quantity, amount }; ask "Add 2 Widgets to INV-003, 10 in total."; post-state unchanged
invoice_with_line_items · line_without_product same hidden write; expected VALIDATION_FAILED writes { invoice, quantity, amount }; still VALIDATION_FAILED (no product), the write_validation guardrail's operation
project_with_tasks · add_task contractor writes estimate_hours: 3, which it must not read; expected applied writes { title, project, status, assignee }; ask drops "3-hour"; post-state unchanged
project_with_tasks · task_without_title same hidden write; expected VALIDATION_FAILED writes { project, status, assignee }; still VALIDATION_FAILED (no title), the write_validation guardrail's operation
travel_expense · employee_approves_colleague → employee_approves_own_claim employee (e02) decides a colleague's step as_112_1 (claim EC-0112, outside its row scope); expected PERMISSION_DENIED decides the manager step as_098_1 of its own claim EC-0098, which it can read; still PERMISSION_DENIED; ask "Approve the manager step of my own claim EC-0098 myself, dated today."; the card's $comment says it checks that a claimant is never the approver. Ruled option A (see Acceptance notes)

Seeds, questions and computed truths are unchanged, so every card stays in its counts band (no question or operation added or removed). test/travel-expense.test.ts names the renamed operation.

packages/qa/scenarios/README.md: the scoring example names remove_ben, the crm card's operation since PR #30801.

Every card in the library now loads with no refusal. The rest passed the rule as is: crm_account_with_contacts, service_desk, requisition_to_receipt and the three mid-tier cards that landed today (order_to_cash, leave_balances, asset_lifecycle) load with no refusal.

Tests

At 72472e89 (this branch, with main merged at 7d9682ff):

Ablation: anchor, blobs and restore
  • Anchor if (actor !== undefined) checkReach(op, actor, at);\n, 1 hit, then 0. Blob 057dc19c to 53539c68.
  • Ran vitest run test/refusals.test.ts test/library.test.ts test/travel-expense.test.ts: Test Files 1 failed, 2 passed (3); Tests 5 failed, 59 passed (64). The five failures are the reach cases: an applied create writing a hidden field, an applied update on an out-of-scope row, a pending delete on an out-of-scope row, a validation refusal on a hidden field, and a refusal code other than the hidden object's.
  • Restored: blob equal to HEAD (057dc19c), git diff HEAD empty.
  • The same five-case ablation was read on every head before this one (f4757520, 4ff318eb, d1909669, b9d68c27, 1bda3e89). On those heads the library still answered the travel refusal, which only the rule produced.

Acceptance notes

Ruled: travel_expense op 4, option A (Elon, 2026-11-06 11:08Z)

The card asked for one measurement against a booted app, and said to stop and report if the two paths contradicted a documented contract. They did, so the operation was held for a ruling. The maintainer ruled A: retarget the operation to a step the employee can see, keep PERMISSION_DENIED, rename it, and leave the seed alone. Done in 72472e89.

Measured with objectstack dev --fresh --no-watch on a throwaway app with the card's shape: approval_step rows with the employee's row scope, the card's agent action decide_approval_step as a declarative update, and persona accounts made with the scenario runner's own HttpApp. Doors: MCP update_record, MCP run_action, REST PATCH /api/v1/data. The employee's edit grant is not something the card can state, so three grant models were measured.

Employee's grant on approval_step colleague's step as_112_1: data update colleague's step: run_action own claim's step: every door
read only (least privilege), on main at c17719dd 403 PERMISSION_DENIED 404 RECORD_NOT_FOUND PERMISSION_DENIED
read only, on main at 7d9682ff (after #30878) 403 PERMISSION_DENIED 403 PERMISSION_DENIED PERMISSION_DENIED
edit, narrowed to the steps it holds (an update rule approver == current user) 404 RECORD_NOT_FOUND 404 RECORD_NOT_FOUND PERMISSION_DENIED
edit with row scope only 404 RECORD_NOT_FOUND 404 RECORD_NOT_FOUND applied (the employee approves its own claim, which the card's prompt rules out)

A missing id got the same answer as the hidden row in every cell. Acting as the employee, get_record on as_112_1 answers RECORD_NOT_FOUND under every model, so an agent acting for the employee could not find the row it was asked to approve.

The first reading showed the two doors disagreeing under least privilege, against content/docs/protocol/objectui/actions.mdx ("fire exactly as for a user edit"). That platform defect was filed as #30848, and PR #30878 corrected it. After that change, the doors agree, but the colleague's step still answers 403 or 404 depending on the edit grant. The employee's own step answers PERMISSION_DENIED at every door under both faithful models, and the agent can read it, which is why A was chosen.

Cost of A, accepted with the ruling: every step of e02's claims is already decided in the seed, so the operation asks to decide a step that its manager already approved. The edit grant is judged before any validation or state rule, so the code does not depend on that. The operation now checks that a claimant is never the approver, not a write across row scope.

The measurement app, the driver and both outputs (measure.out, measure-after-30878.out) are kept with the dispatch records (pm-devx-seat3-30802/measurement/); nothing from them is committed.

Follow-ups (not in this PR, no card filed here)

  • A write across row scope is no longer covered by any operation on this card. Reads across row scope stay covered by the card's permission_parity guardrail. Covering the write needs a card whose persona's edit grant is stated, so its one right code can be declared. It is left for a later card.
  • docs/qa/scenarios/README.md (the format reference) does not yet describe the reach refusal. It is outside this claim's file surface.

Other notes

Verification

Derived gate families at 72472e8: 52 derived, 52 run, all exit 0

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 72472e89 (change set: 7 paths, 109 changed lines (+93, -16) against merge base 7d9682ff, under the 3,000 threshold) derives the same 52 families as on every earlier head of this branch. All 52 ran one at a time at 72472e89, each exit recorded before any pipe. pnpm check:dual-build-cjs-loads, NOT MEASURED on the first run for lack of built output, exits 0 here after the @objectstack/example-todo^... build that the booted-app measurement needed.

--ran reconciliation: "52 derived famil(ies) accounted for — 52 run, 0 NOT-MEASURED (a DERIVED zero — all 52 recorded an exit code and none of them is 3)".

node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

CI on 72472e89: every check run is green, including Test Core (all 8 shards and the aggregate), Lint & Repo Gates, TypeScript Type Check and the PR-body guards; Check Changeset reads skipped (skip-changeset).

main has moved since (1697d5ca, with #30903 changing the scenario runner). A local merge of it into 72472e89 is clean, and pnpm --filter @objectstack/scenarios test passes on it: Test Files 18 passed (18), Tests 251 passed (251). Nothing from it is pushed.


Generated by Claude Code

Every operation of crm_account_with_contacts now acts on what its acting
persona can reach:

- attach_dee (sdr edits ct4, account null, outside sdr's row scope) becomes
  move_eli: sdr re-parents ct5 from Bitworks to Acme, both owned by u_sam,
  still applied with sdr as the audit actor, which is what the audit_actor
  guardrail asserts.
- remove_chen (sdr deletes ct3 at Shopwell, owned by u_tia, outside sdr's
  scope) becomes remove_ben: sdr is asked to delete ct2 at Acme, still
  pending_confirmation.
- add_contact no longer writes contact.email, a field sdr must not read; the
  created row is read back by full_name.
- contact_bad_email runs as partner_user, who can edit contact.email and
  writes no lookup to the account object it must not read, so the refusal
  is still the form's VALIDATION_FAILED the write_validation guardrail asserts.

Seed rows, questions and computed truths are unchanged. The runner and
grader tests that name the renamed operations follow the new ids.

Claude-Session: https://claude.ai/code/session_01Q4EjjPXwX1YfHyKJUh4agu
Co-authored-by: Claude <noreply@anthropic.com>
…(held)

The loader now refuses an operation whose declared outcome contradicts its
acting persona: an object the persona must not read (PERMISSION_DENIED), a
target row outside its row scope (RECORD_NOT_FOUND), or a written field it
must not read (PERMISSION_DENIED). Row scope is decided from the seed rows
through the same relation-path resolver the persona truths use.

Held, not for merge as is: the rule also refuses five operations in three
other library cards (invoice_with_line_items ops 0 and 3, project_with_tasks
ops 0 and 3, travel_expense op 4), which sit outside this claim's file
surface, so the library test is red on this branch until they are fixed.

Claude-Session: https://claude.ai/code/session_01Q4EjjPXwX1YfHyKJUh4agu
Co-authored-by: Claude <noreply@anthropic.com>
…held' into claude/project-thread-9464u3

# Conflicts:
#	packages/qa/scenarios/test/refusals.test.ts
…eir personas may read

add_line and line_without_product no longer write invoice_line.unit_price,
which sales_rep must not read; add_task and task_without_title no longer
write task.estimate_hours, which contractor must not read. Each operation
keeps its persona, its target and its declared outcome. The scenarios
README example names remove_ben, the crm card's operation since its fix.

Claude-Session: https://claude.ai/code/session_016WV4cyjYBCnWT8iGQXxoeH
Co-authored-by: Claude <noreply@anthropic.com>
…iterals

check:dispatcher-error-vocabulary reads the code an object literal stamps
only when its constant resolves to a literal, and ErrorCode.parse(...) does
not. The two constants are now literals typed ErrorCode, so a code the
catalogue drops fails the package's typecheck instead of drifting.

Claude-Session: https://claude.ai/code/session_016WV4cyjYBCnWT8iGQXxoeH
Co-authored-by: Claude <noreply@anthropic.com>
The ErrorCode type admits any string, so a typed literal alone would not
catch a code the catalogue drops. The literals stay (the error-vocabulary
gate reads them) and are parsed once at load, as before.

Claude-Session: https://claude.ai/code/session_016WV4cyjYBCnWT8iGQXxoeH
Co-authored-by: Claude <noreply@anthropic.com>
Measured with a code the catalogue lacks: the typecheck (TS2322), the
error-vocabulary gate and the load-time parse each refuse it. The previous
commit's note that the ErrorCode type admits any string was wrong; its
probe used a code that is in fact registered.

Claude-Session: https://claude.ai/code/session_016WV4cyjYBCnWT8iGQXxoeH
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 6, 2026
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/scenarios, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/qa/scenarios/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6ff27b348e5dba6daa9428251694a7082da3a7ce.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/qa/scenarios/README.md) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 51 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6ff27b348e5dba6daa9428251694a7082da3a7ce → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 679495849ddc9cc19af1b15bf09f73298f5056d1 — the merge of head 72472e8998a429d4adac98250e291e71fb8b3c92 into base 6ff27b348e5dba6daa9428251694a7082da3a7ce, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 679495849ddc9cc19af1b15bf09f73298f5056d1 && git checkout 679495849ddc9cc19af1b15bf09f73298f5056d1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6ff27b348e5dba6daa9428251694a7082da3a7ce 72472e8998a429d4adac98250e291e71fb8b3c92 && git checkout -B drift-repro 6ff27b348e5dba6daa9428251694a7082da3a7ce && git merge --no-ff 72472e8998a429d4adac98250e291e71fb8b3c92

node scripts/docs-audit/affected-docs.mjs --json 6ff27b348e5dba6daa9428251694a7082da3a7ce

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6ff27b348e5dba6daa9428251694a7082da3a7ce → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…own claim's step, so a claimant is never the approver

The operation targeted a colleague's step (as_112_1) outside the employee's
row scope, so its right refusal depended on an edit grant the card cannot
state (PERMISSION_DENIED without one, RECORD_NOT_FOUND with one), and an
agent acting for the employee could not find the row. Ruled option A on
#30802: target the manager step of the employee's own claim EC-0098
(as_098_1), which it can read, keep PERMISSION_DENIED, and rename the
operation employee_approves_own_claim. The seed is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyjYBCnWT8iGQXxoeH
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#3 · os-elon-musk · session_016WV4cz3RTQjLh7hZBLzgtZ

席位核对 · ACCEPT · 72472e89 · 2026-11-06T11:46Z

Head-sha: 72472e8936fb3cd6cfe73fc05f6ad6ff6e1ab3c1

  • Scope: Scenario loader refuses an operation its persona cannot reach (row scope, hidden field or object); fix the three cards it catches #30802. The scenario loader refuses an operation its acting persona cannot reach (checkReach in packages/qa/scenarios/src/loader.ts: hidden object → PERMISSION_DENIED, out-of-scope row → RECORD_NOT_FOUND, hidden written field → PERMISSION_DENIED; codes typed and parsed against ErrorCode), and fixes the five operations it refuses on main. travel_expense op 4 follows the maintainer's ruling (option A, 2026-11-06T11:08Z): employee_approves_own_claim targets as_098_1 of e02's own claim, keeps PERMISSION_DENIED, seed untouched; cross-row-scope write coverage is left for a later card, as the ruling says.
  • Read by the seat: checkReach and the shared inScope, the three card edits, the travel truth pin, the refusal cases. No findings.
  • Shape: 7 files, +93/−16 (109 < 3,000). check-governed-merges --pr 30846: NOT governed. mergeable_state clean. Private package, skip-changeset.
  • Gates: CI on this head green (Test Core 8/8 and aggregate, Lint & Repo Gates, TypeScript, PR size). PR-body guards went red after the body rewrite because check:closing-target-claim read seat 1's yielded claim 6677604452 as governing (its yield is spelled Release:); the seat re-posted its claim (6684032083) and the guard is green on the re-run. Dev: suite 244/244, refusals 53/53, ablation fails exactly the 5 reach cases, dispatch-gates 52/52.
  • Landing: non-governed; landing now under the maintainer's instruction to review and land per pm-dispatch (2026-11-06T03:51Z). Fixes #30802.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scenario loader refuses an operation its persona cannot reach (row scope, hidden field or object); fix the three cards it catches

1 participant