Skip to content

feat(plugin-audit): sys_activity gains writer-stamped read-rule columns with an idempotent backfill, and the withheld-row rule is a predicate with no SYSTEM pre-scan (ADR-0139 E6) - #23814

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-23731-activity-changed-fields
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-23731-activity-changed-fields

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23731

Clause-②: yes

ADR-0139 E6, ruled letter B (ruling record 6205262047, maintainer 「B (推荐)」; Q2 = b confirmed). The activity stream's withheld-row rule becomes a predicate over two writer-stamped sys_activity columns. The SYSTEM pre-scan and its 2,000-row window are gone, for both halves.

Rollout (the changeset's first paragraph)

Run os migrate activity-changed-fields --apply at once after upgrading. Until it runs, stored activity rows carry no stamp and are withheld from every non-system reader. The door's name and flags are os migrate audit-metadata-bodies's shape, one for one (--database-url, --apply, --yes, --force, --json); nothing is renamed, so objectstack-ai/cloud#2900 reads it as declared. It is a dry run by default, idempotent, and covers both columns in one run. One measured difference, stated in the changeset: the columns arrive with the upgraded deployment's schema sync, which --apply performs itself, so a dry run against a database no upgraded process has opened cannot read them and exits 1.

What changed

  • sys_activity gains changed_fields (tags) and milestone_key (text), both readonly. They are stamped by ONE planner, planActivityStamp (activity-changed-fields.ts):

    • changed_fields is the key union of the recorded old / new for an update. It is [] for a create, a delete, a note, or an update that touched only internal fields.
    • milestone_key is the sorted, comma-joined watched-field list (text_sources.kind) of a fired milestone the mirror declared. It is null otherwise.
    • A milestone row that names no readable list cannot be judged. It stays unstamped and fails closed.
  • Writers. The CRUD mirror stamps its own rows. A sys_activity before-insert hook (Q2 = b) stamps every inserted row, over whatever value a writer sent: the engine's create-side readonly strip skips sys_ objects, so the hook is the guard, as comment-access-hooks.ts is for is_edited. Both stamp only when the registered sys_activity declares the columns, the organization_id precedent.

  • The rule (activity-field-redaction.ts). It is now computeWithheldRowFilter → keptRowsOfParent: one branch per parent object the read gate enumerates, or the one parent a read pins on object_name. A row is kept when all of these hold:

    • it is stamped;
    • its changed_fields is empty, or contains a served field (one $contains per served field);
    • its milestone_key is null, or equals the key of P's CURRENT activityMilestones declaration while the reader is served every field that declaration watches.

    It reads no row and composes no leaf. A parent the service gives no answer for is kept whole, as before. An engine with no registry view is denied, and that is said once.

  • The redaction narrows changed_fields to the served members and never serves milestone_key.

  • The query guard (parent-field-query-guard.ts) covers both columns, and the pin against the redaction's fail-closed strip covers them too.

  • The backfill, backfillActivityChangedFields, sits beside stored-metadata-body-migration.ts and is exported from src/index.ts. It reads unstamped rows a page at a time (keyset on id) and updates both columns in one write, as the platform.

  • The door, os migrate activity-changed-fields, is registered in the one-shot family table, the JSON-purity family and the absent-database roster.

  • Docs and generated files. deployment/cli.mdx (a section, and the data-migrations table) and the platform checklist variants (16 → 17 subcommands). The plugin-audit i18n bundles, regenerated and translated for zh-CN / ja-JP / es-ES (the provenance hashes dropped by themselves). The tenant-audit census, regenerated: the backfill's update is one more write call site (225 → 226), and the hand-written prose figures follow. permissions/system-context.mdx row 45 no longer names a pre-scan of the activity redaction.

Landing places: everything is in plugin-audit, the CLI door, dogfood, docs and the generated files the gates regenerate. None of packages/spec, objectql, core, any driver, parent-record-read-gate.ts, comment-access-hooks.ts, or migrate/apply.ts is touched.

Evidence

All readings are at head 70ff93d (merge base 609d07a), on the shared box, under scripts/pm/os-verify-lock.sh.

  • pnpm --filter @objectstack/plugin-audit test: 51 files / 832 tests passed. typecheck exit 0, test-typecheck ledger included.

  • @objectstack/cli typecheck exit 0. Unit project: 297 files / 4,379 tests passed.

  • CLI integration, run locally because the diff touches it:

    • the new door suite: 4 / 4;
    • data-commands.absent-database and schema-migrate.one-shot-family: 135 / 135;
    • json-stdout-purity.e2e: 59 / 59 (nightly tier).
  • @objectstack/dogfood typecheck exit 0.

  • Real stack (dogfood), with the real SecurityPlugin + AuditPlugin and a store of more than 2 × 2,000 rows:

    • a member is served every row it may read past the old window, with that total on every page;
    • an administrator's site-wide read (newest first, top 20) is served the 20 newest rows, and a total equal to the fixture's 4,203 rows plus the rest;
    • no SYSTEM read of the stream reads a row's change, and none carries a ReadableIds leaf. The only SYSTEM read left is the read gate's D5 bounded probe of the parent pair.

    The seven activity dogfood suites, the org-peer sign-in case (activity-withheld-update.dogfood) among them: 7 files / 65 tests passed.

  • The upgrade path, measured. On a database turned back into the previous version's shape (both columns dropped from the rotation shard, the view re-created without them, rows planted unstamped):

    • --apply's plain boot adds both columns to the shard and re-creates the view;
    • it stamps each row exactly as the writer stamps a new one;
    • a re-run stamps nothing, and still counts the one row it cannot judge.

Ablations. Each negative pin was run red, then restored. Every mutation went through scripts/ablation-replace.mjs; restore was proven blob == HEAD with git diff HEAD empty.

leg mutation red
A1 update half always true 9 tests
A2 stamped conjunct dropped 3
A3 milestone key equality loosened 2
A4 changed_fields served unnarrowed 1
A5 milestone_key served 1
A6 hook keeps a writer's own stamp 2
A7 stamped columns dropped from the query guard 1 (the PERMISSION_DENIED / 403 probe)
A8 a SYSTEM pre-scan re-injected into the rule dogfood pin red: a SYSTEM read of id, object_name, metadata carrying a ReadableIds leaf

A8 resolves through dist/. Its marker was present in dist/ (ablation-dist-preflight.mjs), and after the restore and rebuild it was absent. Two attempts were no-ops and are void:

  • A8's first leg: the replacement contained its own anchor, the tool refused it, and the pre-flight showed dist/ unchanged;
  • A5's first leg: refused the same way, so nothing ran.

Acceptance notes


Generated by Claude Code

claude added 11 commits October 16, 2026 02:44
…atabase families

Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy
Co-authored-by: Claude <noreply@anthropic.com>
… backfill's write site; system-context page

Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/plugin-audit, touching 61 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

39 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 d795719926a2dacf7a394e23b717f36961343a26.

⛔ 7 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/plugins/plugin-audit/src/index.ts) — pages documenting those are invisible to this run
  • 11 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 33 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 d795719926a2dacf7a394e23b717f36961343a26 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1a058c00c3aa2ff0b3007b8940a254a4b38a6ea8 — the merge of head d75ae9784332b0ba3d0eeb48efb8d1ee978eed5c into base d795719926a2dacf7a394e23b717f36961343a26, 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 1a058c00c3aa2ff0b3007b8940a254a4b38a6ea8 && git checkout 1a058c00c3aa2ff0b3007b8940a254a4b38a6ea8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d795719926a2dacf7a394e23b717f36961343a26 d75ae9784332b0ba3d0eeb48efb8d1ee978eed5c && git checkout -B drift-repro d795719926a2dacf7a394e23b717f36961343a26 && git merge --no-ff d75ae9784332b0ba3d0eeb48efb8d1ee978eed5c

node scripts/docs-audit/affected-docs.mjs --json d795719926a2dacf7a394e23b717f36961343a26

⚠️ 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 d795719926a2dacf7a394e23b717f36961343a26 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 70ff93d3b03432d2b930d17e2605590f4a174e74
Local-runs: none

Rendered at CONTRACT_REVIEW_TIER by the ruling's item 5 (6205262047 on #23731, letter B, maintainer 「B (推荐)」); the face is the Clause-②: yes declaration (no packages/spec/src/** path, no governed path on the file list), so the tier named above is the face's. Read in this act: card #23731's body and all 10 comments; PR #23814's body, file list (28 paths) and net diff against main (merge base 609d07a7d); the 40 check-runs on the head, latest per name. Scope: every path of the code diff (the two new plugin-audit modules, activity-field-redaction.ts, activity-read-visibility.ts, audit-writers.ts, parent-field-query-guard.ts, objects/sys-activity.object.ts, index.ts, the four generated translations, the five plugin-audit tests, packages/cli/src/commands/migrate/activity-changed-fields.ts with its test and three family registrations, the dogfood pin) plus, read on origin/main: parent-record-read-gate.ts, served-fields.ts, comment-access-hooks.ts, stored-metadata-body-migration.ts, cli/.../migrate/audit-metadata-bodies.ts, cli/src/utils/absent-table-reads.ts, data-migration-plugins.ts, objectql/src/engine.ts and validation/rule-validator.ts (the readonly strips), plugin-security/src/system-write-guard.ts, driver-sql/src/sql-driver.ts and core/src/utils/json-membership-sql.ts, spec/src/data/field-value.zod.ts, ADR-0139, scripts/check-adr-symbol-anchors.mjs. The changeset's prose and the docs are the lane seat's ACCEPT except the changeset's Clause-②: line and level, judged under ②. Stamp of this act: 2026-10-16T04:55Z.

① Derived judgments

  • The two columns on the platform object sys_activity — right. changed_fields (type: 'tags', readonly, not required) and milestone_key (Field.text, readonly, not required), both under the Context group, both arriving through the ordinary schema sync (measured on the rotation shard in activity-changed-fields.integration.test.ts, cases 1 and 2). changed_fields is served narrowed to the reader's served members; milestone_key is deleted from every row the redaction reaches (redactActivityRows, before the parent check) and is therefore never served to a reader the redaction runs for. Both are NULL when unstamped, and [] is stored as a value, not as NULL, which the stamped conjunct relies on.
  • The exported backfill — right. backfillActivityChangedFields(engine, logger, { apply, pageSize }) and ACTIVITY_BACKFILL_PAGE_SIZE, with the two report/engine types, planActivityStamp, the two column constants and ActivityStamp, all added to src/index.ts. It is the shape of migrateStoredMetadataBodyCopies: SYSTEM-context read and single-id SYSTEM update through the engine (the append-only object's migration door, never the CRUD write path), dry run by default, no sys_migration flag, keyset paging on id over changed_fields $null: true, both columns in one update. Idempotent by construction (a stamped row leaves the unstamped set; an unjudged row stays in it and is counted each run). A refused page read is counted as a failure and the door exits 1 — the pin a read the table refuses is a failure, never a clean report holds it.
  • The read rule, both halves, against letter B and ADR-0139 D4 — right. keptRowsOfParent(P, served, watched) is, per parent, object_name = P AND changed_fields IS NOT NULL AND (changed_fields $empty OR one $contains per served field) AND (milestone_key IS NULL, or, only when the reader is served every field of P's CURRENT activityMilestones declaration, milestone_key IS NULL OR milestone_key = key(current declaration)). The update half is D4's sentence exactly (a non-empty changed_fields none of whose members is served is withheld; [] for a create, a delete, a note and an internal-only update is kept). The milestone half is the ruling's item 1 exactly (key equality against the CURRENT declaration AND every watched field served; otherwise withheld; an object with no current declaration serves no milestone row). The unstamped conjunct is D4's unjudged-row answer (fail closed). computeWithheldRowFilter ORs one branch per parent the read gate enumerates (enumerateParentObjects(engine, ACTIVITY_GATE), the gate's own exclusions), or the one parent the read pins (pinnedParentObject, the query guard's reading); a parent the service gives no answer for is kept whole; no answer for any parent returns null (nothing narrowed). An engine with no registry view is denied once and loudly. The predicate compiles to exact membership on driver-sql: tags is in MULTI_OPTION_TYPES, which JSON_COLUMN_TYPES spreads, so $contains takes jsonMembershipPredicate (SQLite json_each, PostgreSQL's jsonb containment operator, MySQL JSON_CONTAINS) and $empty the multi_value arm, on all three modelled dialects; a served field that is a prefix of a withheld one cannot match.
  • No SYSTEM-composed read or leaf remains in the rule — right. The redaction no longer imports CommentAccessEngine's find or PARENT_GATE_SCAN_LIMIT; ActivityRedactionEngine is the registry view plus getSchema. The one SYSTEM read left on the stream is the read gate's own D5 bounded probe of the parent pair, which is ADR-0139 D5's and not this card's; the dogfood pin asserts exactly that and that no SYSTEM read carries a ReadableIds leaf. The 2,000-row window is gone from the rule (activity-withheld-row-predicate.dogfood: a member served past the window with one total on every page; an administrator's top-20 newest with total past the window).
  • The redaction and the query guard cover both columns — right. VALUE_BEARING_COLUMNS carries both, so the fail-closed strip takes them too (activity-predicate-guard.test.ts pins it); ACTIVITY_QUERY_GUARD.columns spreads ACTIVITY_STAMPED_COLUMNS, so a predicate, sort, group or aggregation over either is refused PERMISSION_DENIED / 403 to a reader withheld a parent field (pinned, matching and non-matching probes, find and count).
  • A writer cannot set either column — right. On insert, the sys_activity before-insert hook assigns the planner's answer over whatever any writer sent, in every context (pinned with a SYSTEM-context insert carrying its own values). The engine's create-side static-readonly strip returns no subject for a sys_ object (staticReadonlyInsertSubject), so readonly: true is NOT what guards the columns on insert — the hook is, as the PR says, the is_edited precedent. On update, a user-context write to an append-only object is refused by plugin-security's engine-owned write guard (ADR-0103) wherever the security service is composed; the backfill's SYSTEM update is the one sanctioned writer. The mirror and the hook stamp only when the registered sys_activity declares both columns (the organization_id precedent), read per call.
  • The CRUD mirror's stamp — right. planActivityStamp(activityMetadata) over the same { old, new, kind, text_sources } the row records; the watched list a fired milestone declares now comes from declaredMilestoneWatchedFields(objectDef), the same function the rule keys the CURRENT declaration by, with the same asked predicate (field and summary strings) matchMilestone used on main, so a row written under the declaration in force compares equal.
  • The door os migrate activity-changed-fields — right. Flags one for one with audit-metadata-bodies (--database-url, --apply, --yes, --force, --json), dry run by default on the read-only boot (deferSchemaDdl + readOnlyProbe), absentTableReads for the rotation-managed table, a non-zero failures count exits 1, registered in the one-shot family table, the JSON-purity family and the absent-database roster. The spelling objectstack-ai/cloud#2900 reads is unchanged.
  • The derived gate families on this head — one WRONG. Check-runs, latest per name, none pending at the final read of this act: 34 success (all six Test Core shards among them), 5 skipped (Console Pin Gate, the dogfood matrix shards and Verify CLI, packed-tarball smoke, Temporal Conformance), and two failure: Lint · docs & code guards and its aggregate Lint & Repo Gates. The red step is ADR symbol anchors resolve (no line numbers survive) (scripts/check-adr-symbol-anchors.mjs). Cause, read deterministically off the diff and docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md on main: the diff deletes computeWithheldUpdateFilter and isWithheldOnlyUpdate from activity-field-redaction.ts (replaced by computeWithheldRowFilter and keptRowsOfParent), and ADR-0139 anchors both — line 73 (consumer table, row 3, #computeWithheldUpdateFilter) and line 191 (D4, #isWithheldOnlyUpdate); neither symbol occurs on the head, so both are [unresolved-symbol]. The PR body's accept-set claim that the ADR could be left alone because it is Tier H is wrong as landed: the gate reads the ADR's anchors against the tree, so a rename of an anchored symbol is a change the anchor must follow. The gate's own remedy text: update the anchor (re-point it to where the mechanism now lives) — a docs/adr/** edit, Tier H under Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14 (one Tier H path makes the whole PR Tier H, so it is the maintainer's word on this PR or a sibling PR landed first) — or keep the two symbol names resolvable in the file; ⛔ never an anchor-exempt marker, which is maintainer-only and not for a live anchor. The other anchors into touched files (audit-writers.ts#AUDIT_EXCLUDED_OBJECTS, #auditRow, #diff; activity-read-visibility.ts#installActivityReadVisibility) resolve on the head.
  • Observations, not reasons (the lane's call): (a) an unpinned read builds one $contains leaf per served field per enumerated parent, each an EXISTS, jsonb-containment or JSON_CONTAINS construct, so the WHERE of an administrator's header-bell read grows with parents times fields; measured with one parent in the dogfood, named in the PR body, not measured on an app-sized registry. (b) The pins' field names (awp_open / awp_secret, f_open / f_admin) cannot tell membership from substring; the membership reading is established by driver-sql's construct, not by a pin — one served field that is a prefix of a withheld one would pin it. (c) In a composition with no security service the rule and the redaction do not run, so milestone_key is served there to context-less callers; consistent with the module's standing rule and the no-answer case, stated under ③ Q2.

② Semver level

minor for @objectstack/plugin-audit and @objectstack/cli matches what the diff publishes: two columns on a published platform object, new package exports (the backfill, the planner, the constants and types), a new CLI subcommand, no removal from any public surface (on main, src/index.ts exports nothing from activity-field-redaction.ts, so computeWithheldUpdateFilter, isWithheldOnlyUpdate, isWithheldMilestone, isWithheldRow and ActivityRedactionEngine were package-internal, as the changeset says). Clause-②: yes on the PR body and on the changeset, no arm, and yes takes at least minor — right; not breaking, so no ADR-0087 disposition is owed. The ruling's item 3 binds the changeset's FIRST paragraph and the first paragraph carries it whole: the rollout consequence (rows written before the upgrade are withheld from every non-system reader until the backfill runs), the door os migrate activity-changed-fields, dry run by default, --apply to write, idempotent, both columns in one run, "run at once after upgrading", plus the measured difference (a dry run on a database no upgraded process has opened exits 1). Item 1's cost (older-declaration and since-removed-field milestone rows withheld until retention, 14 days declared, lifecycle.retention_overrides may lengthen it) is stated. packages/qa/dogfood is not a released package.

③ Boundary flags

  • Q1 (the planner's stamp always overrides a writer-sent value) — A, right. It is a superset of the ruling's "for any writer that does not": every writer is stamped, and a writer that stamps is re-derived. B would let any server action relax the read rule for its own rows, which the ruling's floor forbids (item 2: "a row the planner cannot judge stays unstamped and fails closed" presumes the planner's answer is the row's). Pinned (ablation A6; the SYSTEM-context insert carrying changed_fields: [] and a key reads back null / null).
  • Q2 (no security service, or no field answer for a parent: unstamped rows not withheld) — A, right on the merits. The ruling's item 3 sentence "withheld from every non-system reader" is the rollout consequence for a served deployment, where every non-system reader is one the security service answers for; a parent the service gives no answer for has no withheld field to protect, and the module's standing rule — "a reader the service gives no answer for is narrowed by neither", restated in permissions/system-context.mdx row 45 — already serves such a reader every row whole, withheld updates and milestones included. Withholding unstamped rows there would empty every feed of a security-less composition and protect nothing; it relaxes no access boundary, because none exists where no field answer exists. The changeset's paragraph holds for every deployment that composes the security service (the cloud v18 move among them). Accepted.
  • Deviation 1 (docs and generated surfaces outside the claim's list) — accepted: deployment/cli.mdx, system-context.mdx row 45, the tenant-audit census (225 to 226, the backfill's update is the new site) and docs/qa/platform-checklist/areas/cli.json (16 to 17) are gate-regenerated or the lane's prose; the translations are the i18n gates' regeneration (green).
  • Deviation 2 (three family registrations) — right: a new migrate door must join the one-shot family table, the JSON-purity family and the absent-database roster, or those suites red.
  • Deviation 3 (ACTIVITY_GATE exported; the security(plugin-audit): an activity row whose every recorded change is withheld from the reader is still served (empty change, summary, actor, timestamp), so an org peer reads WHEN a colleague's identity row was stamped — each sign-in time #21388 header rewritten) — right: the rule enumerates parents through the gate's own enumeration, one source of truth.
  • Deviations 4 to 7 (package-local tsc and check runs outside the verify lock; the detached primary checkout; the model-free trailer pair; one merge of origin/main) — process notes consistent with AGENTS.md; nothing to answer.
  • The out-of-scope finding (stale prose in parent-record-read-gate.ts and ADR-0139's consumer row 3) — escalated, not noted: the ADR half of it is exactly the anchor that reds this head (①, FAIL reason). parent-record-read-gate.ts's no-object-admission prose stays ADR-0139 E7: the comment-thread, comment-reaction and attachment read gates emit one ReadableIds leaf per parent object and do no pre-scan; D5's three classes keep the probe (D4, D5) #23732's / ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788's. The report's gate line "pnpm check:adr-anchors exited 0" names the presence gate (check-adr-anchors.mjs), not the symbol gate (check:adr-symbol-anchors), and the 103-gate union did not carry the red one; CI did.
  • CI at the final read of this act: every check completed; all six Test Core shards green; every other check green or skipped except the two named reds, which share the one cause above.

Implemented-by: session_01TyhsxCqpTkk1U5GLD1JETy
Reviewed-by: session_018VqJPGA1NDNSCZsnecsChP

VERDICT: FAIL

FAIL reasons:

  1. Gate red on the head: ADR symbol anchors resolve (no line numbers survive) — ADR-0139's anchors activity-field-redaction.ts#computeWithheldUpdateFilter (line 73) and #isWithheldOnlyUpdate (line 191) no longer resolve, because the diff deletes both symbols. Clear it by re-anchoring the ADR (Tier H: the maintainer's word, on this PR or a sibling landed first) or by keeping the two symbol names resolvable in the file; not by an exemption marker. Everything else judged above is right and lands unchanged once the gate is green.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · REWORK (patch round 1) · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T04:56Z

At head 70ff93d3b0. The contract record 6208399171 (FAIL) judges everything right except one gate, and this seat's own read agrees. Form: draft, base main, Fixes #23731, Clause-②: yes; 28 files, all on claim 6206767603's surface or the gate-regenerated and prose surfaces the report lists. check-governed-merges.mjs: NOT governed, 2,367 changed lines. The changeset's first paragraph carries ruling item 3 whole, checked sentence by sentence against the diff. The deployment/cli.mdx row's "No flag" means no migration-flag record, as the summary-nulls row uses it.

Patch round 1, both items in one push:

  1. The red gate (ADR symbol anchors resolve, the only cause of Lint · docs & code guards and Lint & Repo Gates). ADR-0139 cites activity-field-redaction.ts#computeWithheldUpdateFilter (line 73) and #isWithheldOnlyUpdate (line 191), and the diff deletes both. Keep both names resolvable in that file, on the functions that carry those roles now (the withheld-row filter builder; the update-half predicate). ⛔ No alias kept only for the anchor, ⛔ no exemption marker, ⛔ no edit under docs/adr/** (Tier H, the maintainer's hand). Run pnpm check:adr-symbol-anchors locally; it is not in the derived set (dispatch-gates.mjs is frozen by ruling 208, so no card is filed for that).
  2. One membership pin (the record's observation b): a served field whose name is a prefix of a withheld one (f served, f_secret withheld). An update of only f_secret stays withheld; an update of f is kept. Ablate it with a substring match in place of membership; it must go red.

Not asked: observation (a), the WHERE that grows with parents × served fields, stays an Acceptance note with the dogfood's one-parent measurement. The stale prose in parent-record-read-gate.ts stays #23732's / #22788's. ADR-0139's consumer row 3 prose ("its own SYSTEM pre-scan") is now stale Tier H text; this seat raises it to the maintainer at ACCEPT.

A same-form contract review on the new head follows the push.


Generated by Claude Code

…at carry those roles; pin membership over a prefix

computeWithheldUpdateFilter keeps its name as the withheld-row predicate
builder, and isWithheldOnlyUpdate is the update half over changed_fields,
called by keptRowsOfParent. A served field whose name is a prefix of a
withheld one keeps no withheld-only update row.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: d75ae9784332b0ba3d0eeb48efb8d1ee978eed5c
Local-runs: none

Re-review after patch round 1, rendered at CONTRACT_REVIEW_TIER by the ruling's item 5 (6205262047 on #23731, letter B, maintainer 「B (推荐)」); the face is the Clause-②: yes declaration (no packages/spec/src/** path and no governed path on the 28-path file list), so the tier named above is the face's. This record stands on its own: the whole PR is judged at the new head, not only the delta. Read in this act: card #23731's body and all 12 comments (the ruling, the director's delta, the dev's reports 6204491058, 6208152800 and the patch-round report 6208794718, the REWORK pointer 6208416246); PR #23814's body, file list and net diff against main (merge base 609d07a7d); the previous record 6208399171 at 70ff93d3b; the patch round alone (70ff93d3b..d75ae9784: 4 files, +82/-26); the 41 check-runs on the head, latest per name. Scope: every path of the code diff (the two new plugin-audit modules, activity-field-redaction.ts, activity-read-visibility.ts, audit-writers.ts, parent-field-query-guard.ts, objects/sys-activity.object.ts, index.ts, the four generated translations, the five plugin-audit tests, packages/cli/src/commands/migrate/activity-changed-fields.ts with its test and three family registrations, the dogfood pin) plus docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md as the file the two anchors point from, and, read on origin/main: parent-record-read-gate.ts (enumerateParentObjects, andIntoWhere, PARENT_GATE_READ_OPS), parent-field-query-guard.ts#pinnedParentObject, audit-plugin.ts (the middleware registration order), comment-access-hooks.ts (the is_edited precedent), objectql/src/engine.ts and validation/rule-validator.ts (staticReadonlyInsertSubject, stripReadonlyFields), plugin-security/src/system-write-guard.ts, driver-sql/src/sql-driver.ts (JSON_COLUMN_TYPES, the $empty multi-value arm), core/src/utils/json-membership-sql.ts, spec/src/data/filter.zod.ts (the JSON-stored-column membership paragraph), scripts/check-adr-symbol-anchors.mjs and scripts/symbol-anchors.mjs (the declaration rule), .github/workflows/ci.yml (the dogfood shards' condition). The changeset's prose and the docs are the lane seat's ACCEPT except the changeset's Clause-②: line and level, judged under ②. Stamp of this act: 2026-10-16T06:05Z.

① Derived judgments

  • The two columns on the platform object sys_activity — right. changed_fields (type: 'tags', readonly, not required) and milestone_key (Field.text, readonly, not required), both under the Context group, arriving through the ordinary schema sync (measured on the rotation shard by the door's integration test, cases 1 and 2). Served or never served, as the ruling binds: changed_fields is served narrowed to the reader's served members (redactActivityRows filters the array to the served set; a value not in the stored shape is deleted), milestone_key is deleted from every row the redaction reaches, before the parent check, so no reader the middleware runs for is served it (pinned across three readers in activity-milestone-kind.integration). Unstamped is NULL; [] is stored as a value, not as NULL (the backfill integration test's stated question, and the served created / empty rows under the stamped conjunct answer it).
  • The exported backfill — right. backfillActivityChangedFields(engine, logger, { apply, pageSize }), exported from src/index.ts with ACTIVITY_BACKFILL_PAGE_SIZE, its report and engine types, planActivityStamp, the two column constants and ActivityStamp. The shape of migrateStoredMetadataBodyCopies (whose MigrationLogger it imports): a SYSTEM-context find over changed_fields $null: true, keyset on id ascending, projecting id, metadata, and a single-id SYSTEM update writing both columns in one call — the append-only object's migration door, never the CRUD write path. Dry run by default, no sys_migration flag. Idempotent by construction (a stamped row leaves the unstamped set; an unjudged row stays in it and is counted) and pinned (a second --apply run: scanned 1, stamped 0, unjudged 1, failures 0). A refused page read is counted and breaks the walk, so the door exits 1 rather than report a clean table (pinned).
  • The read rule, both halves, against the ruling's letter B text and ADR-0139 D4 — right. keptRowsOfParent(P, served, watched) is, per parent: object_name = P AND changed_fields IS NOT NULL AND isWithheldOnlyUpdate(served) AND milestone-half, where isWithheldOnlyUpdate(served) is changed_fields $empty OR one $contains per served field and the milestone half is, only when P's CURRENT activityMilestones declaration keys and the reader is served every field it watches, milestone_key IS NULL OR milestone_key = key(current declaration), otherwise milestone_key IS NULL. The update half is D4's sentence exactly: an update row with a non-empty changed_fields none of whose members this reader is served is withheld; [] (a create, a delete, a note, an internal-only update) is kept. The stamped conjunct is D4's answer for an unjudged row (closed). The milestone half is the ruling's item 1 exactly: served only when the key equals the CURRENT declaration's key AND every watched field is served; otherwise withheld; an object with no current declaration serves no milestone row (pinned, declaration-edit case included). computeWithheldUpdateFilter ORs one branch per parent the read gate enumerates (enumerateParentObjects(engine, ACTIVITY_GATE): every registered object outside AUDIT_EXCLUDED_OBJECTS and the gated object, read on main) or the one parent the read pins (pinnedParentObject: equality on object_name, through $and only, exactly one pin); a parent the service gives no answer for is kept whole; no answer for any parent returns null; no registry view denies all and is said once. The middleware ANDs it into the WHERE of the four read ops and denies all if the build throws. On driver-sql the predicate is exact membership: tags is in MULTI_OPTION_TYPES, which JSON_COLUMN_TYPES spreads, so $contains compiles through jsonMembershipPredicate (SQLite json_each, PostgreSQL jsonb containment, MySQL JSON_CONTAINS); $empty: true on a multi-value column is col IS NULL OR L (L the dialect's empty-list test), which the $null: false conjunct narrows to exactly []; every arm is total, never UNKNOWN. The spec's membership paragraph binds driver-memory to the same answer.
  • No SYSTEM-composed read or leaf remains in the rule — right. activity-field-redaction.ts no longer imports CommentAccessEngine's find or PARENT_GATE_SCAN_LIMIT; ActivityRedactionEngine is the middleware engine plus the registry view and getSchema. The unit pin asserts engine.find is never called; the dogfood pin (measured on a real SecurityPlugin + AuditPlugin stack of 4,203 fixture rows at 70ff93d3b) asserts the only SYSTEM read of the stream projects object_name, record_id (the read gate's D5 probe of the parent pair, ADR-0139 D5's and not this card's) and that no SYSTEM read carries a ReadableIds leaf; the member is served every row it may read past the old window with one total on every page, and the administrator's newest-20 read totals past 2 × 2,000. The patch round left the rule's output byte-identical (the unit pin one $contains per served field, and the empty set kept asserts the same object literal before and after the extraction), so that measurement stands for this head.
  • The redaction and the query guard cover both new columns — right. VALUE_BEARING_COLUMNS carries both, so the fail-closed strip takes them (pinned in activity-predicate-guard.test); ACTIVITY_QUERY_GUARD.columns spreads ACTIVITY_STAMPED_COLUMNS, so a predicate on either is refused PERMISSION_DENIED / 403 to a reader withheld a parent field, matching or not, on find and count (pinned; control: the administrator filters by changed_fields as by any column). audit-plugin.ts registers the query guard before the read gate and the redaction, so it reads the caller's AST, not the rule's own leaves — which the member's served reads through the real plugin confirm.
  • A writer cannot set either column — right. On insert, installActivityStampHook (a beforeInsert hook on sys_activity) assigns the planner's answer over whatever any writer sent, SYSTEM-context inserts included (pinned twice: a note sent ['f_open'] reads back []; a milestone row sent [] and a key reads back null / null). The engine's create-side readonly strip returns no subject for a sys_ object (staticReadonlyInsertSubject), so the hook is the guard, as comment-access-hooks.ts is for is_edited. On update, the engine's update-side stripReadonlyFields iterates every readonly field with no sys_ skip, so a non-system update loses both columns; and plugin-security's ADR-0103 guard refuses a user-context write to an append-only object wherever the service is composed. The backfill's SYSTEM update is the one sanctioned writer. The mirror and the hook stamp only when the registered sys_activity declares both columns, read per call.
  • The CRUD mirror's stamp — right. planActivityStamp(activityMetadata) over the same object the row serializes into metadata; matchMilestone's watched list now comes from declaredMilestoneWatchedFields(objectDef), the function the rule keys the CURRENT declaration by, with the same asked test (field and summary strings) the mirror used on main, so a row written under the declaration in force compares equal.
  • The two ADR-0139 anchors, this round — right. activity-field-redaction.ts#computeWithheldUpdateFilter (ADR line 73, consumer row 3, "the withheld-update rule") resolves to export async function computeWithheldUpdateFilter(engine, ast, servedFor, logger) at head line 462: the function that builds the withheld-row rule's WHERE for one read, called by the middleware at line 614 — the role the row names. #isWithheldOnlyUpdate (ADR line 191, the function D4 names as the update-half judgement that becomes a predicate) resolves to export function isWithheldOnlyUpdate(served) at head line 391: the update half of the rule as a predicate over changed_fields, called from keptRowsOfParent at line 435 — live on the rule's path, not an alias. Both match the resolver's declaration class (export, optional async, function NAME, after comment stripping), and the head's Lint · docs & code guards and Lint & Repo Gates check-runs are success. No docs/adr/** path on the file list, no anchor-exempt marker in the diff, and computeWithheldRowFilter has no surviving reference at the head (git grep at d75ae9784: 0 hits; keptRowsOfParent is a live helper, not a retired name). Observation, not a reason: the name reads as a boolean "is a withheld-only update" while the value is the kept-rows WHERE — the NOT of it, which is how the ruling's item 4 words the predicate ("NOT-ed into the read") — and the docblock's first sentence says so. The ADR's dated prose at line 73 ("its own SYSTEM pre-scan", a reading at d8830c28) is historical after this PR; the dev carries it to ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788 as stale prose, Tier H, outside this card — the honest route.
  • isWithheldOnlyUpdate does not widen the package's public surface — right. src/index.ts at the head re-exports nothing from activity-field-redaction.ts and holds no export *; the module-level export is package-internal, as keptRowsOfParent, computeWithheldUpdateFilter and redactActivityRows are and were. The changeset's last paragraph says exactly this.
  • The membership pin — right. A parent awu_prefix with f served and f_secret withheld (the served name a prefix of the withheld one); one update of f_secret alone and one of f; the withheld row's changed_fields asserted at rest as ['f_secret']; the member's find excludes the withheld row and includes the served one; the member's count equals the find's length (membership judged by find AND count); control: the administrator is served both. Membership is the driver's construct, and the dev's ablation of driver-sql's $contains arm onto its substring emitter turned exactly this pin red (1 of 27), with the restore proven.
  • The door os migrate activity-changed-fields — right. Flags one for one with audit-metadata-bodies (--database-url, --apply, --yes, --force, --json), dry run by default on the read-only boot (deferSchemaDdl + readOnlyProbe), absentTableReads for the rotation-managed table, a non-zero failures count exits 1, registered in the one-shot family table, the JSON-purity family and the absent-database roster. Measured: a dry run on a database no upgraded process has opened adds no column, reports failures 1 and exits 1; --apply adds both columns to the shard and stamps 4 of 5 planted rows (the unjudgeable milestone row stays unstamped); a re-run stamps nothing. The spelling objectstack-ai/cloud#2900 reads is unchanged.
  • The derived gate families on this head — right. 41 check-runs, latest per name, all completed: 36 success, 5 skipped (Console Pin Gate, the dogfood matrix shards, Dogfood Verify CLI, packed-tarball smoke, Temporal Conformance — opt-in, or excluded on pull_request events by ci.yml's own condition), 0 failure, 0 pending. Among the successes: all six Test Core shards, Check Changeset, Governed Surface Queue Guard, Lint · docs & code guards, Lint & Repo Gates, TypeScript Type Check. check:adr-symbol-anchors is not a derived gate; judged by reading above.
  • Observations, not reasons (the lane's call): (a) an unpinned read builds one $contains leaf per served field per enumerated parent, so an administrator's header-bell WHERE grows with parents times fields; measured with one parent in the dogfood, named in the PR body, not measured on an app-sized registry. (b) Context-less programmatic calls are served milestone_key, the module's standing rule for the read gate too. (c) The definition unavailable fail-closed pin selects its failure by a stack-trace substring (currentMilestoneWatched), test-only brittleness.

② Semver level

minor for @objectstack/plugin-audit and @objectstack/cli matches what the diff publishes: two columns on a published platform object, new package exports (the backfill, the planner, the constants and types), a new CLI subcommand, and no removal from any public surface (src/index.ts on main exported nothing from activity-field-redaction.ts, so the names that changed or went were package-internal). Clause-②: yes on the PR body and on the changeset, no arm, and yes takes at least minor — right; not breaking, so no ADR-0087 disposition is owed. The ruling's item 3 binds the changeset's FIRST paragraph and the first paragraph carries it whole: "Run os migrate activity-changed-fields --apply at once after upgrading", the rollout consequence (rows written before the upgrade carry no stamp and are withheld from every non-system reader), the door's name, dry run by default, --apply to write, idempotent, both columns in one run, the flags one for one, plus the measured difference (a dry run on a database no upgraded process has opened exits 1). Item 1's cost (older-declaration and since-removed-field milestone rows withheld until retention, 14 days declared, lifecycle.retention_overrides may lengthen it) is stated. The patch round reworded only the last paragraph, which now names the kept names accurately against the head. Check Changeset is green. packages/qa/dogfood is not a released package.

③ Boundary flags

  • Q1 (the planner's stamp always overrides a writer-sent value) — A, right; nothing in the patch round touches it. A superset of the ruling's "for any writer that does not": every writer is stamped, and a writer that stamps is re-derived. B would let any server action relax the read rule for its own rows, which the ruling's floor forbids (item 2 presumes the planner's answer is the row's). Pinned (ablation A6; the two SYSTEM-context inserts carrying their own values read back the planner's). The patch round changed no line of activity-changed-fields.ts or the hook.
  • Q2 (no security service, or no field answer for a parent: unstamped rows not withheld) — A, right on the merits; nothing in the patch round changes it. Read against the ruling's item 3 wording "withheld from every non-system reader": that sentence is the rollout consequence for a served deployment, where every non-system reader is one the security service answers for; a parent the service gives no answer for has no withheld field to protect, and the module's standing rule ("a reader the service gives no answer for is narrowed by neither", restated in permissions/system-context.mdx row 45) already serves such a reader every row whole. Withholding unstamped rows there would empty every feed of a security-less composition and protect nothing; it relaxes no access boundary, because none exists where no field answer exists. The patch round's computeWithheldUpdateFilter keeps the no-answer branch ({ object_name: P }) and the no-answer-for-any result (null) unchanged. Accepted.
  • Patch-round deviation 2 (the dogfood suites not re-run at d75ae9784) — accepted by reading. The patch round's code change is a rename plus the extraction of one clause that leaves keptRowsOfParent's output byte-identical (its unit pin asserts the same object literal before and after), the middleware calls the renamed function at the same site, and the dist-resolved dogfood pins read the same rule. The dogfood shards do not run on pull_request events by ci.yml's own condition, so no check-run on either head measured them; the dev's run at 70ff93d3b is the measurement of record, and it stands for this head.
  • Patch-round deviations 1 and 3 (worktree re-created and rebuilt; a worker restart that cut and resumed the gate union, every exit code captured) — process notes; nothing to answer.
  • Patch-round readings: the dev's local check:adr-symbol-anchors run is not a derived gate and is not cited as one here; this record judged the anchors by reading (①) and by the head's two lint check-runs. The membership ablation's void first leg ($icontains on a JSON column measured a driver refusal, not a substring match) is declared as void — right.
  • First report's deviation 1 (docs and generated surfaces outside the claim's list) — accepted: deployment/cli.mdx, system-context.mdx row 45, the tenant-audit census (225 to 226, the backfill's update is the new site) and docs/qa/platform-checklist/areas/cli.json (16 to 17) are gate-regenerated or the lane's prose; the translations are the i18n gates' regeneration (green).
  • Deviation 2 (three family registrations) — right: a new migrate door must join the one-shot family table, the JSON-purity family and the absent-database roster, or those suites red.
  • Deviation 3 (ACTIVITY_GATE exported; the security(plugin-audit): an activity row whose every recorded change is withheld from the reader is still served (empty change, summary, actor, timestamp), so an org peer reads WHEN a colleague's identity row was stamped — each sign-in time #21388 header rewritten) — right: the rule enumerates parents through the gate's own enumeration, one source of truth; activity-read-visibility.ts is not re-exported from index.ts, so the package surface is unchanged.
  • Deviations 4 to 7 (package-local tsc and check runs outside the verify lock; the detached primary checkout; the model-free trailer pair; one merge of origin/main) — process notes consistent with AGENTS.md; nothing to answer.
  • The out-of-scope finding (stale prose in parent-record-read-gate.ts and ADR-0139's consumer row 3) — noted, carrier ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788, right: the anchor half of it no longer reds (①); the prose half is Tier H and the engine lane's, outside this card.
  • CI at the final read of this act: every check completed; none red.

Implemented-by: session_01TyhsxCqpTkk1U5GLD1JETy
Reviewed-by: session_018VqJPGA1NDNSCZsnecsChP

VERDICT: PASS

The previous record's FAIL reasons (6208399171 at 70ff93d3b), each read against the diff at the new head:

  1. "Gate red on the head: ADR symbol anchors resolve (no line numbers survive) — ADR-0139's anchors activity-field-redaction.ts#computeWithheldUpdateFilter (line 73) and #isWithheldOnlyUpdate (line 191) no longer resolve" — cured. The patch round renames computeWithheldRowFilter back to computeWithheldUpdateFilter and extracts the update half as isWithheldOnlyUpdate(served), both exported function declarations on the rule's live path; Lint · docs & code guards and Lint & Repo Gates are success on d75ae9784; no anchor-exempt marker, no docs/adr/** edit, and the retired name has no surviving reference at the head.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T05:53Z

Reviewed against GitHub at head d75ae97843, not against the dev's patch-round report (6208794718 on #23731).

  • Form: draft, base main. The first line is Fixes #23731, then Clause-②: yes; no other card's number sits beside a closing keyword.
    • 28 files: all on claim 6206767603's surface, or the gate-regenerated and prose surfaces the first report lists.
    • Nothing under content/docs/releases/. One changeset: minor for @objectstack/plugin-audit and @objectstack/cli.
    • check-governed-merges.mjs --pr 23814: NOT governed, 2,415 changed lines, under 3,000.
  • Contract review: record 6209058891, PASS, at this exact head.
    • Rendered at CONTRACT_REVIEW_TIER per ruling 6205262047 item 5; Served-tier: TIER_DEFAULT names the declaration face.
    • The previous record 6208399171 (FAIL, at 70ff93d3b) is judged cured. Its one reason was the two ADR-0139 symbol anchors.
  • REWORK 6208409434, both orders met, read on the diff 70ff93d3b..d75ae9784 (4 files, +82/−26):
    1. Both anchored names resolve on the functions that carry those roles. computeWithheldUpdateFilter builds the rule's WHERE and is called by the middleware. isWithheldOnlyUpdate(served) is the update half and is called from keptRowsOfParent. There is no alias, no anchor-exempt marker and no docs/adr/** edit. git grep computeWithheldRowFilter at the head finds 0 hits.
    2. The membership pin.
      • Setup: awu_prefix, where the served field f is a prefix of the withheld field f_secret.
      • What it asserts: the member's find excludes the f_secret-only update and keeps the f update, and count equals that find. The administrator is the control and is served both.
      • Its ablation: driver-sql's $contains arm forced onto its substring emitter, the place where membership is decided. That turned exactly this pin red (1 of 27), and the restore was proven by blob and dist preflight. The dev declared the void first leg ($icontains measured a driver refusal) as void, not as evidence.
  • CI: 41 check-runs, latest per name: 36 success, 0 failure, 0 pending.
    • 5 skipped, all on the roster: Console Pin Gate, Dogfood Regression Gate matrix, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance.
    • The dogfood shards do not run on pull_request. The dev's run at 70ff93d3b stands for this head, because the patch round leaves the rule's output byte-identical (record ③).
  • Changeset prose (this seat's): the first paragraph carries ruling item 3 whole: run os migrate activity-changed-fields --apply at once after upgrading; rows from before the upgrade are withheld from every non-system reader; dry run by default; idempotent; both columns in one run. The patch round reworded only the last paragraph, which now names the kept names and says none of the module is exported from the package. Both read true against the head.
  • Carried, not reasons:
  • Downstream: objectstack-ai/cloud#2900 reads the door's name, os migrate activity-changed-fields, which is unchanged.

ACCEPT. This act marks the PR ready and enables auto-merge through the fleet relay. It lands through the merge queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 05:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 05:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 5b6c257 Oct 16, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23731-activity-changed-fields branch October 16, 2026 06:24
This was referenced Oct 16, 2026
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/xl tests tooling

Projects

None yet

2 participants