Skip to content

ADR-0139 E6: sys_activity gains a writer-stamped changed_fields column with an idempotent backfill, and the withheld-update rule becomes a predicate with no SYSTEM pre-scan (D4) #23731

Closed

Description

@objectstack-fleet

Ruled: 6205262047 · letter B · 2026-10-16T00:33Z

carries item E6 of #22788; #22788 keeps item E7 (cut beside this one)

Filing gate: ④ a coordination node's execution card, row E6 of ADR-0139's execution plan. It is cut by #22788's owner (domain:engine seat 1, #6367 · sunhaolin · session_01GP3comALHRcRBixS2TEnLF) now that its predecessor has landed. It lands in packages/plugins/plugin-audit, another lane. Per the split rule it carries no domain:*: triage sets the lane and the grade on first touch. ⛔ Classes, positions and functions only.

Predecessor landed: E5 (#23673), PR #23693 → 69351e985a. computeParentRecordFilter now emits one ReadableIds leaf per parent object and does no pre-scan. D5's three classes keep the bounded probe. Earlier: E1 (#22787), E2 (#22861), E3 + E4 (#23639, PR #23656 → 462dff36f1).

Reader who acts: triage, to set the lane (by the lane table, plugin-audit is domain:services) and the grade; then that lane's seat.

The work (ADR-0139 D4; the plan's row E6)

  • packages/plugins/plugin-audit/src/objects/sys-activity.object.ts#sys_activity gains a writer-stamped column, changed_fields: the key set of the recorded change. The CRUD mirror stamps it from the same packages/plugins/plugin-audit/src/audit-writers.ts#diff that composes the change.
  • The withheld-update rule (packages/plugins/plugin-audit/src/activity-field-redaction.ts#computeWithheldUpdateFilter; today it judges #isWithheldOnlyUpdate over the metadata text column) becomes a predicate. An update row is withheld when its changed_fields is non-empty and this reader is served none of its members. Its SYSTEM pre-scan, and the 2,000-row window that pre-scan composes the gate's leaves under, are gone.
  • Stored rows are backfilled from their metadata.old / metadata.new keys by one idempotent stored-row migration, in the shape of packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts#migrateStoredMetadataBodyCopies. The object is managedBy: 'append-only', so the backfill goes through the migration door, never the CRUD write path. A row the backfill cannot judge keeps the fail-closed answer it has today.
  • ADR-0139's Consequences: "changed_fields is a schema change on an append-only engine-owned object, with a backfill a deployment must run; the changeset of that card says so."
  • Clause-② check: this adds a column to a published platform object. The claiming seat judges the line.

Pins

Notes carried from E5's report (inputs, noted on #22788, not filed)

  • plugin-security's explain answers a SYSTEM context allowed: false while canReadObject answers true. So a leaf composed in a SYSTEM read selects everything or nothing, depending on which half the service offers. E5 guards its own consumer by writing no leaf without canReadObject. This card removes a SYSTEM pre-scan and should leave no SYSTEM-composed leaf behind.
  • The engine composes each leaf with the outer read's full envelope, operation-private keys included (__readScope). Observation only.

Readings this card owes

  • O2 (ADR-0139): the activity measurement is the only measured one. This card's pins read it end to end.

Governing text

ADR-0139 D4 (the withheld-update rule and changed_fields) and its Consequences; the plan's row E6; #22788's amended pin set 6197472558 and the director's pin pointer 6115710018.

Dedupe

MCP search_issues on this repo, closed included: "ADR-0139 E6 changed_fields sys_activity writer stamp backfill withheld-update rule predicate" returns 57 hits. The nearest are #22788 (the parent), #22590 (the original report), and #22678 (the record's own card, closed). None is E6.

Dedupe words: ADR-0139 E6 changed_fields; withheld-update rule predicate no SYSTEM pre-scan; sys_activity backfill migration.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    First grade · domain:services · p2 · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T22:59Z

    Read in this act: the body in full, 0 comments.

    ⛔ Not a claim.

  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-15T23:07Z
    Session: session_017WLmd5in9RXu7AjyHR8fKQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23731-activity-changed-fields
    Worktree: objectstack-issue-23731
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface: in packages/plugins/plugin-audit/src/: objects/sys-activity.object.ts (the changed_fields column), audit-writers.ts (the CRUD mirror's stamp, from diff), activity-field-redaction.ts (computeWithheldUpdateFilter becomes a predicate; its SYSTEM pre-scan and 2,000-row window go), and one new stored-row backfill module beside stored-metadata-body-migration.ts with its export in src/index.ts. Also their tests, dogfood pins under packages/qa/dogfood/test/, and one changeset that names the schema change and the backfill a deployment must run. Possibly one new subcommand under packages/cli/src/commands/migrate/ (and its test) as the backfill's door, mirroring audit-metadata-bodies.ts; declared to domain:cli's seat (#6024) in this round. Also whatever generated files the gates regenerate. ⛔ Not packages/spec, packages/objectql, packages/core or any driver. ⛔ Not parent-record-read-gate.ts, comment-access-hooks.ts or service-storage's attachment-access-hooks.ts (#23732's). ⛔ Not the pinned-record timeline (#22788's). Stop on breach; explain in the report.
    Container & model: L, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). This seat owes one CONTRACT_REVIEW_TIER review before the queue, by its own judgment: the card rewrites an access-control read rule and adds a column to a platform object.
    Clause-②: yes (a served, writer-stamped field changed_fields on the platform object sys_activity, and an exported backfill function on @objectstack/plugin-audit; no packages/spec path)
    Responsibility: plugin-audit (this lane): computeWithheldUpdateFilter judges isWithheldOnlyUpdate over the metadata text column under a SYSTEM pre-scan of at most 2,000 rows, so it caps the activity stream (member 725 of 1,816 in E5's real-stack measurement 6196293752) | ADR-0139 E1–E5 landed the leaf and the activity gate's switch (E5: 69351e985a), which this card's consumer still caps | an administrator's header-bell and home-page activity reads, and every member's broad activity read
    Thread-read: 6204041849
    Serial constraints cleared: no open PR touches packages/plugins/plugin-audit/**, packages/services/service-storage/**, packages/cli/src/commands/migrate/** or packages/qa/dogfood/test/** (all 4 open PRs' file lists read at this write). E5 landed as 69351e985a. #23732 runs in parallel from this seat on disjoint files, as triage's first grade 6204041849 names the split.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23731,
      "status": "needs_decision",
      "branch": "claude/issue-23731-activity-changed-fields",
      "pr": null,
      "session": "session_017WLmd5in9RXu7AjyHR8fKQ (subagent run: the parent PM session's id, as this run's Claude-Session line names it)",
      "premise_still_valid": true,
      "summary": "Stopped before code, per the dispatch's assumption 1a: the milestone half of the withheld-row rule cannot become a predicate without a column or a ruling the card does not give. The premise holds on origin/main 3b46855fb9. computeWithheldUpdateFilter (activity-field-redaction.ts) still pre-scans the read's WHERE as SYSTEM at PARENT_GATE_SCAN_LIMIT (2,000, imported from parent-record-read-gate.ts) on the four PARENT_GATE_READ_OPS reads. It judges isWithheldRow = isWithheldOnlyUpdate OR isWithheldMilestone. The file is unchanged since f313fbcc99, so E5's real-stack cap (6196293752: member 725 of 1,816, administrator total 1,999) was measured against this code. WHY THE MILESTONE HALF FORKS. (1) ADR-0139 was accepted at a2e94c2a05. The milestone row-withholding (#22786) landed later the same day as f313fbcc99 (commit dates), so D4's text covers only the update half. (2) No column tells a fired milestone from a tracked update. The mirror writes type = the declaration's own type, or 'updated' (activityTypeFor). The kind and its watched-field list exist only inside the metadata JSON text, as metadata.kind and metadata.text_sources.kind. (3) The two halves test different things. The update half withholds a row when NONE of its changed fields is served. That can be written exactly as one $notContains per served field, against a multi-value changed_fields column, where $contains/$notContains answer membership on driver-sql (all three dialects), driver-memory and driver-mongodb (filter.zod.ts membership-contract docblock). The milestone half withholds a row when SOME watched field is not served. That can only be written as one $contains per field the reader is not served. The only finite list of those fields is the system's readable fields of the parent minus the reader's served set, and a watched field since removed from the parent is in no such list. So Q1 below is for the maintainer: one of its options relaxes an access-control edge, and another relaxes a ruled boundary. WHAT IS NOT IN DOUBT, and would land unchanged under any answer to Q1 or Q2: (a) the changed_fields column; (b) the mirror's stamp, which is the union of the keys of the recorded oldValue / newValue (diff after ledgerView), so an update that touched only internal fields stamps [] and keeps its row; (c) the predicate on the update half, NOT-ed into the read per enumerated parent, with no SYSTEM read and no window; (d) the field redaction narrowing changed_fields to served members, and the query guard refusing a predicate on it. Without (d), a member's changed_fields $contains probe on a served mixed row would count changes to a withheld field. (e) The backfill module beside stored-metadata-body-migration.ts, exported from src/index.ts. (f) Its own door, os migrate activity-changed-fields (domain:cli, declared at 6204194217). It mirrors audit-metadata-bodies: dry run by default, absentTableReads for the rotation-sharded table. A door is owed because ADR-0139's Consequences say a deployment must run the backfill. (g) changeset minor, Clause-②: yes. A CONSEQUENCE TO READ WITH Q1: under the lane's floor, a row the backfill has not judged is never served. So after an upgrade, and until the operator runs the backfill, every historical activity row is withheld from every non-system reader, because rows at rest carry no stamp. Feeds show only new rows until the backfill runs or retention ages the old rows out. The changeset must say this. Effort for the whole card, by source reading: about 1,200 to 1,800 lines with tests, plus about 200 for Q1 option A or B. The worktree is removed; the remote branch is the empty probe push at main 3b46855fb9.",
      "tests": "None run. No code was written (needs_decision), so there is nothing to test, ablate or measure, and every real-stack pin is NOT MEASURED. The pins are: 1,808 of 1,808; the director's pair (administrator top-20 total 4,201; a member past the window); idempotence; an unjudged row staying fail-closed; no SYSTEM-composed leaf. Reason: the switch they measure is not built. Readings instead, each against origin/main 3b46855fb9. activity-field-redaction.ts: computeWithheldUpdateFilter lines 401-457, isWithheldMilestone 361-370, the middleware 548-592. audit-writers.ts: diff 454-478, ledgerView 1627-1690 (it omits internal fields after masking), the activity row 2092-2125, matchMilestone 823-842 (watched = every asked milestone's field, deduped). sys-activity.object.ts: managedBy append-only, retention maxAge 14d. lifecycle-service.ts: retention_overrides can lengthen retention per tenant. stored-metadata-body-migration.ts: SYSTEM-context engine.update rewrite, idempotent, no sys_migration flag. packages/cli/src/commands/migrate/audit-metadata-bodies.ts: the door shape. parent-field-query-guard.ts: the column guard a new column must join. Writers of sys_activity: git grep finds one in-tree writer, audit-writers.ts line 1150. In-tree milestone declarations: one, examples/app-showcase/src/data/objects/task.object.ts (field status, value done). Assumption checks: 1 confirmed (source); 1a confirmed, and it is Q1; 2 confirmed (servedFieldsPerRead -> resolveServedFields, shared with the ledger redaction; the predicate would ask it once per enumerated parent per read, where today it asks once per parent present among the 2,000 candidates); 3 partly falsified, and it is Q2; 4 confirmed (the CLI dynamic-imports migrateStoredMetadataBodyCopies from @objectstack/plugin-audit, so the backfill takes the same route through its own subcommand); 5 NOT MEASURED, reason above.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, exit code captured before any pipe: EXIT=2. Its first line: 'dispatch-gates: gate list derived from the tree of objectstack-ai/objectstack at commit 3b46855fb9'. Its answer: 'this branch changes nothing against origin/main (merge base 3b46855fb) - nothing to derive'. With no change set, no local gate is owed, no --ran reconciliation applies, and with no PR there are no check-runs to cite. UNRECOGNISED lines: none (no gate ran). The PM's lead gates-23731.txt was not used: there is nothing to derive it against.",
      "mcp_calls": "0",
      "api_writes": "1: this report, POST /repos/objectstack-ai/objectstack/issues/23731/comments through scripts/pm/post-stamped.mjs (the fleet-write relay). pr_create 0 and label-write 0: there is no PR. git push: the empty branch only, as the write-route probe (not a REST write). Reads: REST GETs of the card and its 2 comments; #22788 and its comments (including 6197472558, 6115710018, 6113122770 and 6203616563); #23693's 6196593905; #23673's 6196293752; #22590 and its comments (including 6097072172 and 6113111917); #22678's comments.",
      "open_questions": [
        {
          "question": "Q1 (maintainer). How is the MILESTONE half of the withheld-row rule withheld once the SYSTEM pre-scan goes? Today (#22786, f313fbcc99) a fired milestone row is withheld from a reader who is not served every field the parent's activityMilestones declaration watched at write time. A milestone row whose list cannot be read is withheld from a reader restricted on that parent. ADR-0139 D4 predates that rule and names only the update half. On today's columns it cannot be a predicate. No column marks a milestone row: type is the declaration's own type, or 'updated'. The kind and its list live only in the metadata JSON text. And its test, 'some watched field is not served', can be written only over the fields the reader is NOT served.",
          "options": [
            "A: A second writer-stamped multi-value column holding a fired milestone's watched fields (today's metadata.text_sources.kind list). The CRUD mirror stamps it, the same backfill fills it, and it is never served; the redaction and the query guard cover it. Predicate: a milestone row of parent P is withheld when its column contains any field of P that the security service reads for the system but does not serve this reader (one $contains per such field). Exact for every watched field P still has. A watched field since REMOVED from P is in no such list. So a milestone row naming that field is served, where today it is withheld, to a reader served its other watched fields, for up to the retention window (14 days as declared; lifecycle.retention_overrides can lengthen it). That one edge is a relaxation, and it is why A is the maintainer's. Axes: real need - one in-tree declaration (showcase task, field status); no measured deployment restricts a watched field. Long-term - both halves become predicates: no window and no SYSTEM re-read, the ADR's direction. AI-error - writer-stamped, judged mechanically, no metadata parsing at read time. Startup focus - one more column and one more backfill arm in the same migration and the same operator run. Cost: about 200 lines over the E6 PR.",
            "B: The same second column, stored as one sorted text key of those watched fields. Predicate: a milestone row of P is withheld unless its key equals the key of P's CURRENT activityMilestones declaration AND this reader is served every field that declaration watches. That is one null/not-equal leaf per parent, with no JSON membership. It never serves a row today's rule withholds. Its cost is narrowing, never a leak: a milestone row written under an earlier declaration, or naming a since-removed field, is withheld from every reader, administrators included, until retention ages it out. Axes: real need - as A. Long-term - as A, and judged against the declaration that exists now. AI-error - the strictest column option: no edge serves what today withholds, and its one cost shows as fewer rows. Startup focus - the same size as A, with a cheaper predicate. Cost: about 200 lines.",
            "C: Keep a bounded SYSTEM probe for milestone rows only, named in code (ADR-0139 D5's posture). Its candidates need a selector. A column means a second schema change anyway, so B dominates it. A substring match over the metadata JSON text is a workaround. C also keeps the 2,000-row window for those rows (fail-closed past it) and one SYSTEM-composed read, which goes against E5's carried note. Axes: real need - low pull either way. Long-term - keeps one bounded consumer of the class the ADR removes. AI-error - the substring selector is the brittle shape. Startup focus - about 150 lines.",
            "D: Retire the row-level withholding of milestones and keep only the kind's column redaction. The row's type and summary (the declaration's own) still tell a reader who is not served a watched field that the field entered its declared value. This relaxes #22786's ruled boundary, so it is the maintainer's. Axes: startup focus - cheapest (about -80 lines, two pins rewritten); everything else against."
          ],
          "recommendation": "B. Real need: the pull is one declared milestone in the showcase, so the answer must be cheap and must not relax anything; A and B cost the same. Long-term: A and B both finish the ADR's direction (no window, no SYSTEM re-read). C keeps a bounded consumer the ADR exists to remove, and D drops a ruled guard. AI-error: B is the only column option under which no row today's rule withholds is ever served, and its one cost is visible (fewer rows after a declaration edit), never a leak. Startup focus: one migration and one operator run carry both columns. If the maintainer prefers exactness for current fields and accepts the removed-field edge, A."
        },
        {
          "question": "Q2 (seat-decidable: no option relaxes access). What answer does a row get when a writer other than the CRUD mirror inserts it after this change, so that it carries no changed_fields? Dispatch assumption 3 puts such a row on the fail-closed branch. Measured: the predicate cannot tell an unstamped update from an unstamped note, because the shape lives only in metadata. So fail-closed would withhold EVERY row such a writer writes (notes, emails, calls), not only its update rows. In-tree the mirror is the only writer (audit-writers.ts line 1150). But sys-activity.object.ts names an app's own server action (ctx.api.object('sys_activity').insert, measured in objectstack-ai/hotcrm) as a real channel, and the seed loader can write rows.",
          "options": [
            "a: Fail closed for every unstamped row. It is withheld from every non-system reader, administrators included, unless the writer stamps changed_fields itself ([] for a non-update), and the changeset tells app authors so. Axes: real need - empties the measured app channel's rows from every feed, a narrowing no ruling asked for. Long-term - one rule, no hook. AI-error - the silent trap: an AI writing an app action sees its rows vanish with no error. Startup focus - no extra lines.",
            "b: Stamp at insert for every writer that does not. A sys_activity before-insert hook in plugin-audit derives changed_fields from the row's own metadata.old / metadata.new keys, using the backfill's own planner, so stored rows and new rows are judged by one derivation. A row whose metadata the planner cannot judge stays unstamped and takes the fail-closed answer. Axes: real need - keeps the app channel's rows exactly as they are served today. Long-term - judgement moves to write time (the ADR's direction), with one derivation. AI-error - an author needs no knowledge of the column. Startup focus - about 40 lines and one pin, no new surface.",
            "c: Refuse loudly. An insert into sys_activity without changed_fields from anyone but the mirror answers 400, naming the column. Axes: loud, but it breaks every existing app write, and a new refusal is a new surface to keep."
          ],
          "recommendation": "b. Neither a nor b relaxes access, so the seat can decide this without the maintainer. b keeps today's answer for every row a writer can be judged by, and fails closed only where ADR-0139 D4 says a row keeps its fail-closed answer. a narrows a measured channel silently, and c breaks it loudly."
        }
      ],
      "out_of_scope_findings": [],
      "line_budget": "n/a: no diff (no skills/** or governed path touched).",
      "deviations": "1) The PM's mechanism assumption 3 is reported as partly falsified (Q2), not implemented. 2) Assumption 5's real-stack pins were not reproduced, because nothing was built; each is NOT MEASURED with its reason in tests. 3) pnpm install ran outside the verify lock, as installs do; nothing ran under the lock. 4) The worktree's node_modules were removed and the worktree deleted before this report; there were no commits and no uncommitted work.",
      "files_changed": []
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    待维护者裁决:E6 去掉 SYSTEM 预扫后,「里程碑记录」那一半的隐藏规则怎么判

    domain:services 席位 1(#6021)· os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T23:37Z。依据 dev 报告 6204491058 立卡,席位已在 origin/main 上逐条复核(见「前提」)。⛔ 未写任何代码;远端分支只有一次空推探针。

    一句话问题: 活动流里有一类「某字段达到了声明的值」的里程碑记录。今天,读者只要有一个被监视的字段看不到,这条记录就整条对他隐藏(#22786 的安全修复)。ADR-0139 D4 要求去掉读时的系统预扫,改成按列判断,但它只写了「更新记录」那一半。里程碑这一半在现有列上无法判断,所以要选:补一列(两种写法),保留一个有界探针,或撤掉这条隐藏规则。

    Governing text

    前提(各附复核命令)

    1. ADR-0139 合入(a2e94c2a05,10-11 06:02Z)早于里程碑隐藏规则(f313fbcc99,10-11 09:40Z)。复核:git log -1 --format=%cI a2e94c2a05 f313fbcc99。
    2. 今天的判断式是 isWithheldOnlyUpdate(…) || isWithheldMilestone(…),两者都读 metadata 文本。复核:grep -n "isWithheldMilestone\|isWithheldOnlyUpdate" packages/plugins/plugin-audit/src/activity-field-redaction.ts。
    3. 没有任何一列能区分里程碑记录:type 取声明自己的类型,否则是 updated(activityTypeFor)。kind 和被监视字段只存在 metadata 文本里。复核:grep -n "function activityTypeFor" -A3 packages/plugins/plugin-audit/src/audit-writers.ts。
    4. 仓内只有一处里程碑声明:examples/app-showcase/src/data/objects/task.object.ts(activityMilestones)。复核:git grep -n activityMilestones -- examples。

    选项 × 真实代价 × 工作量

    选项 做什么 客户能感知的后果 工作量
    A 再加一列,写入时记下里程碑监视的字段。读时只要读者看不到当前对象上的某个被监视字段,就隐藏 现存字段判得精确。有一处放宽:一个被监视字段后来从对象上删掉了,对应的里程碑记录会给看得到其余字段的读者看到;今天它是隐藏的。在保留期内都如此(声明 14 天,可按租户延长) E6 本体约 1,200–1,800 行(含测试)外加约 200 行。先例:E5 PR #23693 +1,488/−221
    B 同样加一列,但存成一个排序后的字段键。读时只有「键等于对象当前声明的键,且读者看得到当前声明监视的全部字段」才放行 永不放行今天隐藏的记录。 代价是收紧:声明改过之后,旧声明写下的里程碑记录,以及点名了已删字段的记录,对所有非豁免读者隐藏,直到保留期把它们清掉 同 A,约 +200 行,判断式更便宜
    C 只给里程碑记录保留一个有界的系统探针(D5 的写法) 里程碑记录仍受 2,000 行窗口限制,超出部分隐藏,读时还要再做一次系统读。选候选要么还得加列(那 B 更好),要么在 JSON 文本上做子串匹配(变通写法) 约 150 行
    D 撤掉里程碑的整条隐藏,只保留 kind 列上的遮蔽 推翻 #22786 的安全修复:看不到监视字段的读者,会从记录的 type 和 summary 读出那个字段达到了声明的值 约 −80 行,改写两个 pin

    业务含义直译:

    • A 像「按每个字段的现行权限判断」,但对已经删掉的字段失明。
    • B 像「旧版规则写的记录,规则一改就一律封存到过期」。
    • C 像「新系统上线了,单留一个旧窗口专门处理这类记录」。
    • D 像「把一道已经修过的门重新打开」。

    四轴(业务立场)

    • 项目长远合理性(权重 ≥50%): A、B 都把两半都变成写入时打标、读时按列判断,没有窗口,也没有系统重读,正是 ADR 的方向。C 保留了 ADR 要消除的那一类读法。D 让安全修复倒退。终态句:两年后,平台的活动流可见性全部由写入时打下的列来判;主流平台(例如 Salesforce 的字段历史)按「每个字段的现行字段权限」决定一条变更能不能看到。A 更贴近这个模型,B 更保守。
    • 实际业务拉动: 仓内只有一处里程碑声明(showcase 的 task status)。没有任何已测量的部署限制过被监视字段。拉动接近零,所以不应扩散,也不应放宽。
    • 防 AI 犯错: 出错时谁看到什么?B 出错只会让读者「少看到几条」,是看得见的收紧,永不静默泄露。A 的边缘是静默放行。C 依赖 JSON 子串选择器,最脆弱。D 是确定的静默泄露。
    • 创业阶段不扩散: A、B 都是同一次迁移、同一次运维操作带两列,增量相同。C 少一列,但保留一个永久的特殊读路径。D 最省事,但触及人工地板。

    推荐 + 回退 + 置信缺口

    • 推荐 B。 只看①:A 与 B 都完成了 ADR 的方向。A 更贴近「按现行字段权限」的主流模型,但带一处放宽。这条车道的地板是:放宽访问边界只能由维护者定,席位不推荐放宽。所以推荐落在 B。
    • 回退 A:维护者若接受「已删字段」这一处放宽,换取对现存字段的精确判断。
    • 置信缺口: 看不到真实部署多常改里程碑声明。这正是 B 的代价(改声明后旧里程碑记录全被封存),仓内也无从测量。
    • 自检: 只看①会在 A、B 间接近平局,略偏 A。②③④ 和地板把推荐定在 B。按规则,这不翻字母:推荐是 B,这里把偏向如实写出,由维护者裁。

    席位已自行裁定的子问题(Q2,不涉访问放宽,维护者可否决)

    问题:非 CRUD 镜像的写入方(应用自己的 server action、seed loader)插入的行没有 changed_fields,怎么判?

    • 裁定 b:在 plugin-audit 里给 sys_activity 挂一个 before-insert 钩子,用回填同一套推导,从该行自己的 metadata.old / metadata.new 键推出 changed_fields。推不出来的行不打标,走 fail-closed。
    • 理由: 这条路对每条可判断的行保持今天的答案,只在 D4 所说「回填判断不了的行」上 fail closed。不选 a,因为 a 会把应用写的备注、邮件、通话等所有行静默隐藏;不选 c,因为 c 会响亮地拒掉现有的每一次应用写入。

    维护者须知的上线后果(与选项无关,changeset 必须写明)

    升级之后、运维跑完回填之前,存量活动行都没有打标,会对所有非系统读者隐藏,活动流只显示新行。这一点由 ADR 的「a backfill a deployment must run」决定。dev 会新增一个回填入口 os migrate activity-changed-fields(cli 车道,已在 #6024 申报,6204194217),与 audit-metadata-bodies 同形:默认 dry run。changeset 定为 minor,并点名「升级后必须立即运行」。

    裁后执行

    os-decision-facets

    Release: session_017WLmd5in9RXu7AjyHR8fKQ (domain:services seat 1) releases claim 6204160151 · reason: a fork the maintainer must rule (Q1) · destination: needs-user-decision. The assignee is cleared in this act. The branch claude/issue-23731-activity-changed-fields holds only the empty probe push.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Box intake (triage): the face is complete and ready for the director. The decision is the domain:services seat's 6204549006, built on the dev's report 6204491058. It recommends B, with A as the fallback; the seat already answered Q2 itself (b), and the maintainer may veto that. The labels stand: needs-user-decision, domain:services and p2.

    Read for intake:

    • the one-line question (一句话问题);
    • Governing text;
    • the premises, each with its re-check command;
    • the options with real cost and workload;
    • the four axes;
    • the recommendation, fallback, confidence gap and self-check;
    • the sub-question the seat answered (Q2);
    • the rollout consequence the changeset must state;
    • 裁后执行;
    • the os-decision-facets block;
    • Prior rulings read:.

    None is missing.

    For the director:

    • A relaxes an access boundary. A milestone row whose watched field was later deleted becomes readable. That is on the human floor, and the seat does not recommend it.
    • The rollout consequence holds under every letter. Until a deployment runs the backfill, every stored activity row is hidden from non-system readers. That reaches objectstack-ai/cloud's v18 move as a step it must run.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T23:58Z. ⛔ Not a claim.

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见(增量)· director seat (summon #42, continuation 1, session_01AKVFxqmRLqktJspVXwPE9A), read as batch #353 item 1, 2026-10-16T00:22Z

    On the domain:services seat 1 presentation 6204549006 (B recommended, A the fallback; Q2 self-ruled b), the dev's readings 6204491058 and triage's intake 6204805947.

    Premises re-run in this act, origin/main 140238a47b:

    Facets: ① agree: A and B both finish the ADR's direction (writer-stamped columns, no window, no SYSTEM re-read); A is the closer fit to the mainstream per-field model, B is the stricter; C keeps a bounded consumer of the class the ADR removes; D re-opens a closed security fix · ② agree: one in-tree declaration and no measured deployment restricting a watched field, so near-zero pull: neither widen nor relax · ③ agree: B's only failure mode is fewer rows, visible; A's edge is a silent relaxation (a milestone row that names a since-removed watched field becomes readable to a reader served the other fields, for the retention window); D is a certain silent disclosure · ④ agree: A and B ride one migration and one operator run; C keeps a permanent special read path.

    Recommend B; fallback A (the maintainer's call alone: A relaxes an access boundary, by one edge, for up to the retention window). Not C, not D. 口径 for the executor, binding with the letter:

    1. Q2 = b is confirmed (one derivation at write time, the backfill's own planner, in a sys_activity before-insert hook in plugin-audit; an unjudgeable row stays unstamped and fails closed).
    2. The rollout consequence binds the changeset: after the upgrade and until the operator runs the backfill, stored activity rows are hidden from every non-system reader; the changeset says so in its first paragraph and names the door os migrate activity-changed-fields (the domain:cli half, already declared to seat [PM seat] domain:cli — 🟢 marchtian · session_01PFB9ZGRWYvjhTXsd8i6KBp · finishing 3, then serial (maintainer) #6024 at 6204194217), dry run by default. objectstack-ai/cloud's v18 move must run it; the maintainer carries that note to the cloud lane (outside this session's repositories).
    3. Under B, the second column is one sorted text key of the fired milestone's watched fields; a milestone row of parent P is served only when its key equals the key of P's CURRENT activityMilestones declaration AND the reader is served every field that declaration watches; the column is never served; the redaction and the query guard cover both new columns. Its cost (older-declaration rows withheld from every non-exempt reader until retention, 14 days declared) is stated in the changeset.
    4. Unchanged under any letter, as the report lists: the changed_fields column and the mirror's stamp; the update-half predicate NOT-ed per enumerated parent with no SYSTEM read and no window; the field redaction and query guard on changed_fields; the backfill module and its door; Clause-②: yes; the pins (1,808 of 1,808; the director's pair; idempotence; an unjudged row fails closed; no SYSTEM-composed leaf).
    5. One PR for the card (domain:services, with the cli door declared cross-lane), contract review at CONTRACT_REVIEW_TIER by the seat's own judgment (an access-control read rule and a platform-object column).

    Workload: B = E6's about 1,200–1,800 lines with tests plus about 200 (the dev's reading; precedent E5 PR #23693 +1,488/−221); A = the same; C about 150; D about −80.

    自检:只看①在 A 与 B 之间,略偏 A;②③④ 定为 B(③ 的「永不静默放行」破平局),不翻字母。

  7. 1 remaining item

  8. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer: the cloud v18 move step for ruling item 3 is filed as objectstack-ai/cloud#2900 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T00:46Z

    • Why. Item 3 of the ruling 6205262047 left the cloud note for the maintainer to carry. The maintainer handed it to this seat, verbatim: 「ADR-0139 E6: sys_activity gains a writer-stamped changed_fields column with an idempotent backfill, and the withheld-update rule becomes a predicate with no SYSTEM pre-scan (D4) #23731 落地后,云端 v18 迁移必须在升级后立刻跑 os migrate activity-changed-fields,否则活动流只显示新行。」
    • The cloud card:
      • is pm:blocked on this card, with p1 and target:v18;
      • is read by the repo:cloud seat that owns the v18 move (cloud#2709, cloud#2868);
      • runs the backfill on every database that carries sys_activity in the deploy's migrate phase, with a post-check and a rehearsal on a copy.
    • For this card's PR: the cloud card uses the door's name and flags as this PR ships them. If the spelling changes from os migrate activity-changed-fields, the changeset's first paragraph is where the cloud seat reads it.

    ⛔ Not a claim.

  9. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-16T02:35Z
    Session: session_018VqJPGA1NDNSCZsnecsChP (the dispatching seat; the dev runs as a cloud session that seat post #21118 names)
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23731-activity-changed-fields
    Worktree: objectstack-issue-23731 (in the cloud session's container)
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface (at origin/main d3df3ba24b or later; stop on a breach and explain it in the report): in packages/plugins/plugin-audit/src/: objects/sys-activity.object.ts (changed_fields and the milestone key column, both writer-stamped), audit-writers.ts (the CRUD mirror's stamp), activity-field-redaction.ts (both halves become predicates; the SYSTEM pre-scan and the 2,000-row window go), the field redaction and parent-field-query-guard.ts (both new columns), a sys_activity before-insert stamping hook (Q2 = b), and one backfill module beside stored-metadata-body-migration.ts exported from src/index.ts. Also their tests, dogfood pins under packages/qa/dogfood/test/, one new door os migrate activity-changed-fields under packages/cli/src/commands/migrate/ with its test and registration (domain:cli, declared to #6024 in this round), one minor changeset, and the generated files the gates regenerate. ⛔ Not packages/spec, packages/objectql, packages/core or any driver. ⛔ Not parent-record-read-gate.ts or comment-access-hooks.ts (#23732's E7 half, in the decision box). ⛔ Not the pinned-record timeline (#22788's). ⛔ Not packages/cli/src/commands/migrate/apply.ts while PR #23778 is open on it.
    Container & model: L, mode:cloud, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"); ceiling CONTRACT_REVIEW_TIER for the review owed before the queue, reason: the ruling's item 5 (an access-control read rule and platform-object columns)
    Clause-②: yes
    Responsibility: plugin-audit (this lane): computeWithheldUpdateFilter judges both halves over the metadata text under a SYSTEM pre-scan of at most 2,000 rows, so it caps the activity stream (member 725 of 1,816 in E5's real-stack measurement 6196293752) | ADR-0139 E1–E5 landed the leaf and the activity gate's switch (E5: 69351e985a), which this consumer still caps | an administrator's header-bell and home-page activity reads, and every member's broad activity read
    Thread-read: 6205429823
    Ruling-ref: 6205262047 (letter B, maintainer 「B (推荐)」; Q2 = b confirmed; the changeset's first paragraph names the backfill door); the director's delta 6205124261; the cloud step objectstack-ai/cloud#2900 (6205429823) reads the door's name and flags as this PR ships them.
    Serial constraints cleared: open PRs read in this act; none touches packages/plugins/plugin-audit/** or packages/qa/dogfood/test/'s activity files. PR #23778 touches migrate/apply.ts and one meta-* test, both excluded above. #23732 (E7) is released into the decision box (6205516105) and holds no file. Read on origin/main d3df3ba24b: no commit touched plugin-audit or commands/migrate since the dev's reading at 3b46855fb9, and changed_fields has 0 hits in plugin-audit/src (control activityMilestones in audit-writers.ts: 3). The predecessor's empty probe branch of the same name stays as the starting point.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23731,
      "status": "done",
      "branch": "claude/issue-23731-activity-changed-fields",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23814",
      "session": "session_01TyhsxCqpTkk1U5GLD1JETy (cloud dev session; the dispatching seat is session_018VqJPGA1NDNSCZsnecsChP, claim 6206767603)",
      "premise_still_valid": true,
      "summary": "Ruling 6205262047 (letter B; Q2 = b) is implemented. sys_activity gains two readonly columns: changed_fields (tags) and milestone_key (text). Both are stamped by one planner, planActivityStamp, called from three places: the CRUD mirror, a before-insert hook on sys_activity that stamps every writer's row over any value the writer sent, and the backfill. The withheld-row rule is now computeWithheldRowFilter / keptRowsOfParent: one branch per enumerated parent, or the one parent a read pins. A row is kept when it is stamped, AND its changed_fields is empty or contains a served field (one $contains per served field), AND its milestone_key is null or equals the key of P's CURRENT activityMilestones declaration with every watched field served. There is no SYSTEM pre-scan and no window. The redaction narrows changed_fields and never serves milestone_key, and the query guard covers both columns. backfillActivityChangedFields is exported from src/index.ts, and its door is os migrate activity-changed-fields, which has audit-metadata-bodies' flags, is a dry run by default and is idempotent. The changeset is minor, and its first paragraph says to run the door at once after upgrading. The real upgrade path was measured: on a previous-version database, the --apply boot adds both columns to the rotation shard and re-creates the view. A dry run on a database no upgraded process has opened cannot read the columns and exits 1; the changeset names this.",
      "tests": "Head 70ff93d3b, merge base 609d07a7d. Every run below went through os-verify-lock, on a shared box. (1) plugin-audit: pnpm test passed 51 files / 832 tests, and typecheck (test-typecheck ledger included) exited 0. (2) cli: typecheck exited 0; vitest --project unit passed 297 files / 4,379 tests. (3) cli integration (the diff touches it): the new door suite activity-changed-fields.integration passed 4/4, and data-commands.absent-database + schema-migrate.one-shot-family passed 135/135. json-stdout-purity.e2e ran under OS_TEST_TIERS=nightly (the file is nightly-tier by name) and passed 59/59. (4) dogfood: typecheck exited 0. The new activity-withheld-row-predicate.dogfood ran on a real SecurityPlugin + AuditPlugin stack holding 4,203 fixture rows, more than 2 x 2,000. A member is served every row it may read past the old window, and every page reports the same total. An administrator's site-wide read (newest first, top 20) gets the 20 newest rows and total = 4,203 + the rest. No SYSTEM read of sys_activity reads a row's change or carries a ReadableIds leaf; the only SYSTEM read left is the read gate's D5 parent-pair probe. Seven activity dogfood files passed 65/65, among them the org-peer sign-in case (activity-withheld-update.dogfood), activity-parent-read-gate, activity-text-predicate and audit-log-audit-capability. (5) Ablations: each leg went through scripts/ablation-replace.mjs, and each restore was proven blob == HEAD with git diff HEAD empty. A1 (update half made always-true): 9 red. A2 (stamped conjunct dropped): 3 red. A3 (key equality loosened): 2 red. A4 (changed_fields served unnarrowed): 1 red. A5 (milestone_key served): 1 red. A6 (hook keeps a writer's stamp): 2 red. A7 (stamped columns dropped from the guard): 1 red, the PERMISSION_DENIED/403 probe. A8 (SYSTEM pre-scan re-injected, dist-resolved): the dogfood pin went red with a SYSTEM read of id,object_name,metadata carrying a ReadableIds leaf; ablation-dist-preflight read the marker present 0 / absent 0 after restore and rebuild. Two first attempts were no-ops, void and declared: A5 and A8 each had a replacement that contained its own anchor. The tool refused both; for A8, dist/ was proved unchanged by the pre-flight.",
      "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths). First line: 'gate list derived from the tree of objectstack-ai/objectstack at commit 70ff93d3b'; change set 28 paths vs merge base 609d07a7d. The union ran on the final head 70ff93d3b, each exit code captured before any pipe: 103 of 103 exited 0. --ran: 'Run reconciliation — 103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero; every line carried its exit code). The earlier pass at the pre-fix head had seven non-zero. Four were fixed in this diff: check-tenant-audit-census and its --self-test exited 1 (the backfill's update is one more write call site, 225 → 226; regenerated, and the prose figures moved), check:i18n exited 1 and check:i18n-coverage exited 1 (the new field labels; regenerated and translated). Three were prerequisites that measured nothing: the plugin-teardown-shape --self-test fixture commit was missing from the shallow clone (fetched by sha); spec check:skill-examples and check:dual-build-cjs-loads exited 3 on unbuilt packages (built: the eight packages, then pnpm build). Also run because the diff edits its subjects: pnpm check:adr-anchors exited 0. UNRECOGNISED lines are the standing verdict-only ones (engine-double-contract 22 constructs; durability-read-invention not applicable), unchanged by this diff. CI on PR 23814: 24 check-runs at report time, 2 completed, 22 in_progress, 0 failure. Recorded as in_progress; this report does not wait for CI.",
      "mcp_calls": "0",
      "api_writes": "2. (1) pr_create through the fleet-write relay (scripts/pm/fleet-write/dispatch.mjs, transport dispatch per --route): POST /repos/objectstack-ai/objectstack/dispatches (204). Run 38851106712 executed POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/23814/assignees ([os-elon-musk]). Read-back: 8,329 bytes sent, 8,329 stored, identical. (2) This report: POST /repos/objectstack-ai/objectstack/issues/23731/comments through scripts/pm/post-stamped.mjs. label-write: 0 (the dispatch's budget names none). git push of the branch is not a REST write. Reads: REST GETs of the card and its 9 comments, comment 6196293752 on #23673, and the PR's check-runs.",
      "open_questions": [
        {
          "question": "Q2's literal text says the hook stamps 'for any writer that does not'. Should a writer that sends its own changed_fields / milestone_key keep them?",
          "options": [
            "A (taken): the planner's answer always wins. A stamp can never disagree with the row's metadata, so an app's own row cannot carry changed_fields: [] on an update of withheld fields and be served. This is the is_edited precedent in comment-access-hooks.ts: the engine's create-side readonly strip skips sys_ objects.",
            "B: keep a writer-sent value. This would let any server action relax the read rule for its own rows."
          ],
          "recommendation": "A. It is a superset of the ruling's text: the planner stamps every writer that does not, and re-derives for any writer that does. B relaxes an access edge, which the ruling's floor forbids. Pinned by ablation A6."
        },
        {
          "question": "A deployment with no security service, or a parent the service gives no field answer for: should unstamped rows be withheld there too? Ruling item 3 says 'withheld from every non-system reader'.",
          "options": [
            "A (taken): no field answer means nothing is narrowed for that parent, unstamped rows included. This is the module's standing rule ('a reader the service gives no answer for is narrowed by neither') and today's answer: with no security service, the redaction serves every value whole.",
            "B: withhold unstamped rows even without a security service. Every activity feed of a security-less deployment would be empty until the backfill runs."
          ],
          "recommendation": "A. With no field-level security there is no field to protect, so withholding protects nothing and only empties the feeds. The changeset's rollout paragraph holds for every deployment that composes the security service (every served deployment with an auth secret)."
        }
      ],
      "out_of_scope_findings": [
        "carrier: #22788 (domain:engine) · noted, not filed · stale prose, no wrong answer: parent-record-read-gate.ts's 'no-object-admission' whole-read-probe reason, and its WHOLE_READ_PROBE_WHY text, say the withheld-update rule re-reads the gate's WHERE as SYSTEM. After E6 that rule reads no row, so the reason no longer holds; whether the no-canReadObject whole-read probe can now become leaves is engine-lane work. The file is #23732's, outside this card's surface. ADR-0139's consumer table (row 3, #computeWithheldUpdateFilter; Tier H) carries the same stale anchor. Dedupe words: no-object-admission whole-read probe after E6; WHOLE_READ_PROBE_WHY withheld-update SYSTEM re-read stale; ADR-0139 consumer 3 computeWithheldUpdateFilter"
      ],
      "line_budget": "2,367 changed lines (git diff --numstat 609d07a7d..70ff93d3b: 2,020 additions + 347 deletions, 28 files, generated i18n bundles and census tables included). Under the 3,000 human-merge threshold, so the PR body carries no 维护者速读 section. No governed path.",
      "deviations": "1) Three file surfaces outside the claim's named list, each because a gate or a hand-written page reads the changed behaviour: content/docs/deployment/cli.mdx (the door's section and the data-migrations row); content/docs/permissions/system-context.mdx row 45 (it named the activity redaction's pre-scan); content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (census regenerated, 225 → 226, prose figures moved); docs/qa/platform-checklist/areas/cli.json (variants 16 → 17). Also the plugin-audit translations/*.generated.ts. 2) Three family registrations of the door, one each in data-commands.absent-database.integration.test.ts, schema-migrate.one-shot-family.integration.test.ts and json-stdout-purity.e2e.test.ts. 3) activity-read-visibility.ts now exports ACTIVITY_GATE, so the rule enumerates the gate's own parents; its #21388 header paragraph is rewritten. 4) Several package-local tsc --noEmit runs and the check:* union ran outside the verify lock, as the rules allow. 5) The primary checkout was detached (git checkout --detach) so the branch could be checked out in the dedicated worktree. 6) The harness's attribution reminder asks for a model-named Co-Authored-By trailer; every commit carries the model-free pair AGENTS.md requires instead. 7) One merge of origin/main (8 commits, no overlap with this diff), then §9's install and closure rebuild.",
      "files_changed": [
        ".changeset/23731-activity-changed-fields.md",
        "content/docs/deployment/cli.mdx",
        "content/docs/permissions/system-context.mdx",
        "content/docs/permissions/tenant-audit-census.mdx",
        "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
        "docs/qa/platform-checklist/areas/cli.json",
        "packages/cli/src/commands/migrate/activity-changed-fields.ts",
        "packages/cli/src/commands/migrate/activity-changed-fields.integration.test.ts",
        "packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts",
        "packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts",
        "packages/cli/test/json-stdout-purity.e2e.test.ts",
        "packages/plugins/plugin-audit/src/activity-changed-fields.ts",
        "packages/plugins/plugin-audit/src/activity-changed-fields-backfill.ts",
        "packages/plugins/plugin-audit/src/activity-changed-fields-backfill.integration.test.ts",
        "packages/plugins/plugin-audit/src/activity-field-redaction.ts",
        "packages/plugins/plugin-audit/src/activity-milestone-kind.integration.test.ts",
        "packages/plugins/plugin-audit/src/activity-predicate-guard.test.ts",
        "packages/plugins/plugin-audit/src/activity-read-visibility.ts",
        "packages/plugins/plugin-audit/src/activity-withheld-update.integration.test.ts",
        "packages/plugins/plugin-audit/src/audit-writers.ts",
        "packages/plugins/plugin-audit/src/index.ts",
        "packages/plugins/plugin-audit/src/objects/sys-activity.object.ts",
        "packages/plugins/plugin-audit/src/parent-field-query-guard.ts",
        "packages/plugins/plugin-audit/src/translations/en.objects.generated.ts",
        "packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts",
        "packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts",
        "packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts",
        "packages/qa/dogfood/test/activity-withheld-row-predicate.dogfood.test.ts"
      ]
    }

    Generated by Claude Code

  11. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    REWORK (patch round 1) → PR #23814 comment 6208409434 · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T04:57Z. The contract record 6208399171 failed on one gate (two ADR-0139 symbol anchors); everything else was judged right.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23731,
      "status": "done",
      "round": "patch round 1 (orders: contract record 6208399171 FAIL, seat REWORK 6208409434, both on PR 23814)",
      "branch": "claude/issue-23731-activity-changed-fields",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23814",
      "head": "d75ae9784332 (previous head 70ff93d3b; one commit, 4 files, +82/-26)",
      "session": "session_01TyhsxCqpTkk1U5GLD1JETy",
      "summary": "Both items are in one push. (1) ADR-0139's two anchors resolve again, on the functions that carry those roles; there is no alias, no exemption marker and no docs/adr edit. computeWithheldRowFilter is renamed back to computeWithheldUpdateFilter, the withheld-row predicate builder, under one name. isWithheldOnlyUpdate(served) is the update half of the rule over changed_fields: the empty set or one $contains per served field, written as the rows it keeps. keptRowsOfParent calls it, so the name is the live clause. The changeset's last paragraph is reworded to say so. (2) The membership pin: a parent with a served field f and a withheld field f_secret. An update of only f_secret is withheld from the member, by find and count, and is served to the administrator. An update of f is kept.",
      "readings": {
        "adr_symbol_anchors": "pnpm check:adr-symbol-anchors, run locally at d75ae9784 (it is not in the derived set): exit 0, '2348 anchors across 142 records resolve — 451 symbol (424 declaration, 27 literal), 1869 file-level, 28 cross-repo, 6 exempt, 18 continuation'. On the PR, the head's check-runs are complete: 36 success, 5 skipped, 0 failure. Lint · docs & code guards and Lint & Repo Gates are among the successes.",
        "membership_pin_ablation": "Substring in place of membership, ablated where membership is decided: driver-sql's $contains arm forced onto its substring LIKE emitter for changed_fields. The ablation is temporary and dist-resolved. ablation-replace confirmed the mutation landed (anchor 1 -> 0). driver-sql was rebuilt, and ablation-dist-preflight found the marker present in dist (exit 0). The run went red on exactly 1 of 27: the new pin, 'expected [...] to not include' the f_secret-only row. Restore: blob == HEAD with git diff HEAD empty, rebuild, and the preflight found the marker absent (exit 0). A first leg that swapped $contains for $icontains in the rule is VOID. The SQL driver refuses a text operator on a JSON column, so that run measured a refusal, not a substring match: 15 red, all one 'WAS NOT APPLIED' error. Restored the same way."
      },
      "tests": "At d75ae9784, under os-verify-lock: plugin-audit pnpm test passed 51 files / 833 tests (one more than before, the new pin), and typecheck (test-typecheck ledger included) exited 0. The edited suites alone: activity-withheld-update.integration + activity-milestone-kind.integration passed 49/49.",
      "gates": "Re-derived at d75ae9784 (dispatch-gates --commands --repo objectstack-ai/objectstack; 28 paths vs merge base 609d07a7d): 103 commands. All 103 exited 0, each code captured before any pipe. The runner was cut at 85 by a worker restart and resumed on the 18 not yet run, with the same capture. --ran: 'Run reconciliation — 103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN'. Before the union: pnpm build (75 tasks, 64 cached) and the fixture commit fetched by sha.",
      "line_budget": "2,415 changed lines vs merge base 609d07a7d (2,072 + 343), under 3,000.",
      "mcp_calls": "0",
      "api_writes": "1: this addendum, POST /repos/objectstack-ai/objectstack/issues/23731/comments through scripts/pm/post-stamped.mjs. git push of the branch (not a REST write). The PR body is not PATCHed. Reads: the two PR comments named above, and the head's check-runs.",
      "deviations": "1) The worktree was re-created from the remote branch for this round, then rebuilt (closure, then pnpm build). 2) The dogfood suites were not re-run at d75ae9784: the change renames a function and extracts one clause unchanged, and the dist-resolved dogfood pins read it through the same rule; they ran green at 70ff93d3b. 3) One worker restart cut the union, which resumed as described under gates."
    }

    Generated by Claude Code

  13. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23814 comment 6209078746 · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T05:53Z. Contract record 6209058891 PASS at d75ae97843; landing through the merge queue.


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T06:25Z.

    probe reading
    PR #23814 timeline added_to_merge_queue 2026-10-16T05:56Z → merged 2026-10-16T06:24Z through the merge queue
    merge commit on origin/main 5b6c257b06 (git merge-base --is-ancestor exit 0)
    the rule on origin/main export async function computeWithheldUpdateFilter in activity-field-redaction.ts → 1; changed_fields in objects/sys-activity.object.ts → 2; control: computeWithheldRowFilter under packages/ → 0
    • Records: contract 6209058891 (PASS at d75ae97843), ACCEPT 6209078746.
    • State: the card closed completed by Fixes #23731. pm:dispatched and the assignee are cleared in this act. The mode:cloud dev session is archived.
    • Downstream:

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions