Skip to content

crm_event_attendee.invited_date is the one audit stamp left without a writer census — the reason it stays open was retired by #1667 #1747

Closed

Description

@os-bill

Filed by the repo:hotcrm seat (session session_019YKN9TPqDSSXUaYqp2j7MZ, R55) out of the review of PR #1737. ⛔ Not a defect — an audit debt with a named owner-shaped hole in it.

Dedup: run against the open+closed set with a working control — the search returned #1667 (the sibling, closed) and nothing else, so the zero for this column is a real zero rather than a broken query.

What happened

PR #1737 declared readonly: true on the three columns ruled by decision batch #74 (#1666: crm_opportunity.approval_status / approved_date; #1667: crm_campaign_member.added_date).

Two files outside that ruling carried comments citing crm_campaign_member.added_date as the precedent for staying open. #1667 overturned that precedent, so those comments became false the moment the PR landed. The dev corrected both — and on src/objects/event_attendee.object.ts it wrote this, which is why this card exists rather than a fourth flag flip:

⛔ NOT readonly — but the cross-reference this note used to carry is retired, not merely reworded … the reason it cited ("the engine strips a readonly key the CALLER supplied") does not reach an INSERT at all — the strip is an UPDATE-path rule. What holds this column open today is that nobody has ruled on it. ⛔ Do not flip it on the strength of the sibling ruling: #1666 / #1667 were two separate product decisions about two specific columns, and this one has had neither. It needs its own writer census and its own ruling.

⭐ That refusal was correct and is the reason this is a clean card: the column's stated justification is now known to be wrong, and its actual justification is "unexamined".

What this card is

Do for invited_date exactly what the #1435 audit did for the other three: a per-writer census. That is the whole deliverable.

For each writer of crm_event_attendee.invited_date, record: what writes it, whether the write is an INSERT or an UPDATE, its effective runAs, and therefore whether it would survive a readonly: true declaration. The card body's own starting point (⛔ unverified, re-measure): the retired comment claimed it is "written by the activity actions on insert".

The two mechanisms the sibling cards turned on, both already pinned in test/readonly-write-semantics.test.ts and test/audit-stamp-readonly.test.ts:

  • the readonly strip is an UPDATE-path rule — INSERTs are never reached;
  • elevation is not anonymity (objectstack#5494) — a runAs: 'system' run carries isSystem: true, so the strip branch is skipped.

⛔ Scope — this card does NOT flip the flag

⛔ Declaring readonly is a behaviour change and a product decision, exactly as #1666 and #1667 were. This lane has now executed that principle three times and must not execute it a fourth way.

⇒ The output of this card is the census, filed back onto this card, and then:

⛔ Either way the flag is not touched in this card's PR.

Note for whoever takes it

⚠️ src/objects/event_attendee.object.ts also has an open question one layer down that this census will meet: #1078 (making the attendee form structurally unable to build a wrong-shaped row) is pm:blocked on the same file's shape. ⛔ Do not widen into it.

Refs #1666 · #1667 · #1435 · PR #1737

Activity

  1. added
    metadataDeclarative metadata — schema, security posture, UI surfaces
    pm:queueReady for the PM dispatch loop
    on Sep 7, 2026
  2. added theissue type on Sep 7, 2026
  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 8, 2026
  4. self-assigned this
    on Sep 8, 2026
  5. os-bill commented on Sep 8, 2026

    @os-bill
    CollaboratorAuthor

    Claim: session session_019YKN9TPqDSSXUaYqp2j7MZ · seat repo:hotcrm · R56 wave 2 · branch claude/issue-1747-invited-date-writer-census

    PM dispatch. The assignee and this claim are written by the seat; the dev inherits both, verifies this is the newest Claim: and that it names its branch, and ⛔ posts no second claim and ⛔ never writes the assignee.

    File surface src/objects/event_attendee.object.ts plus a read-only census. No fence collision — src/translations/** is held by #1717 and content/docs/** by #1729, both disjoint. ⚠️ #1078 is pm:blocked on this same file's shape: ⛔ do not widen into it.


    Generated by Claude Code

  6. huangyiirene commented on Sep 8, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round R57
    Session: session_01PpRjGNnwyo2J1rrmekxB1W
    Branch: claude/issue-1747-invited-date-census
    Worktree: hotcrm-issue-1747
    Domain: repo:hotcrm (single-lane repo — no domain:*)
    File surface: src/objects/event_attendee.object.ts (comment only, if anything at all) + a read-only census across writers + .changeset/ (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default judgement tier — ⚠️ ⛔ not a --tier reading: dispatch-gates.mjs REFUSES for this repo (no scripts/pm/ here), so the tier is this seat's explicit decision
    Clause-②: no
    Thread-read: 5579891175
    Serial constraints cleared: this card holds src/objects/event_attendee.object.ts. ⚠️ #1078 is pm:blocked on the same file's shape ⇒ ⛔ do not widen into it. In-flight sibling PR #1768 (#1717) is src/translations/** only ⇒ disjoint, verified from its get_files. No other in-flight card touches src/objects/.


    ⚠️ Re-claim after a takeover — ⛔ this supersedes the Claim: at 5579891175

    Provenance (who / verbatim / where): maintainer, live PM chat in this session, 2026-09-08T10:0xZ, choosing between three seating shapes — 「坐席并接管三张在飞卡」.

    The dispatching seat session_019YKN9TPqDSSXUaYqp2j7MZ claimed this card at 05:42:19Z and has produced nothing in the lane since 05:56Z. ⛔ This seat has NOT judged it dead — none of the three death criteria is met, and get_session / list_sessions are unreadable for that session (it runs under the os-bill account), ⛔ not negative. The takeover is the maintainer's instruction.

    Measured state of the previous attempt, ⛔ not inferred:

    branch claude/issue-1747-*   →  ABSENT
       control probe: claude/issue-1573-schema-validation-section resolves ⇒ the zero is real,
       not a broken query.
    Report: absent.
    

    ⚠️ As on #1729, an absent branch ⛔ does not establish that no local work exists; that container is unreachable from here, so no WIP rescue is possible. A fresh branch name is used deliberately so that a returning dev's "newest Claim: names my branch?" check fails loudly rather than silently sharing a branch.

    Premise re-check at dispatch time — HOLDS (measured on origin/main @ a02d4c14, 2026-09-08T10:2xZ)

    src/objects/event_attendee.object.ts :343-346  — the retired-precedent comment is verbatim as the card quotes it:
       "What holds this column open today is that nobody has ruled on it. ⛔ Do
        not flip it on the strength of the sibling ruling… It needs its own
        writer census and its own ruling."
    :347  invited_date: Field.datetime({ group: 'response', label: 'Invited' })   ⇒ still NO readonly
    CONTROL: 'readonly' occurs 4× elsewhere in this same file ⇒ the grep shape works,
             so the absence on invited_date is a real absence.
    

    ⇒ The card's whole premise — the stated justification is retired, the actual one is "unexamined" — is intact.


    Generated by Claude Code

  7. huangyiirene commented on Sep 8, 2026

    @huangyiirene
    Collaborator

    Writer census — crm_event_attendee.invited_date

    Measured on claude/issue-1747-invited-date-census @ 347d0b8 (branched from origin/main @ 2e0f4dd). PR #1777 (draft) carries the comment-prose half. ⛔ The flag is not touched.

    Result

    # Writer Verb Effective runAs / context Survives readonly: true?
    1 The fifteen generated activity actions — 5 ACTIVITY_TARGETS x 3 ACTIVITY_SPEC_NAMES, one shared body at src/actions/global.actions.ts:338 INSERT isSystem: true Yes — exempt twice over
    2 eventAttendeesFromContacts + eventAttendeesFromLeads, records built by attendeeRecords() at src/data/service.seed.ts:980 INSERT on first load, UPDATE on mode: 'upsert' replay isSystem: true Yes — INSERT exempt, and the replay UPDATE skips the strip branch
    3 The record form — src/views/event_attendee.view.ts:97 lists invited_date in the invitation section INSERT (create) and UPDATE (edit) user context, not isSystem INSERT half yes — UPDATE half NO

    ⇒ A user-context UPDATE writer exists. By this card's own routing that is the branch where the column legitimately stays open, and where the card closes having replaced "nobody has ruled on it" with a measured reason.

    Evidence, per writer

    1 — the activity actions. One statement, shared by all fifteen actions:

    const doc = Object.assign({}, a, { crm_event: eventId, invited_date: nowIso });
    const row = await ctx.api.object('crm_event_attendee').insert(doc);
    

    The context is measured on the pinned engine rather than assumed — node_modules/@objectstack/runtime/dist/index.js:2581:

    function buildActionExecutionContext(ec) {
      const base = ec && typeof ec === "object" ? { ...ec } : {};
      return { ...base, isSystem: true };
    }

    2 — the service seed. ⚠️ Not a pure INSERT: both seeds are mode: 'upsert', so a warm-boot replay against an existing row is an UPDATE carrying invited_date. It survives anyway, because seed writes run isSystem — the repo's own load-bearing claim at src/flows/demo-bootstrap.flow.ts:86 ("seed writes run under { isSystem: true }"), corroborated by the behaviour that claim exists to explain: seeded rows landing ownerless, pinned at test/flow-scheduled.test.ts:786.

    3 — the record form. invited_date sits in the invitation section beside response and is_organizer. Two things make it a writer rather than an artefact of the sweep:

    • the membership is authored, not synthesized — src/views/event_attendee.view.ts exists and someone put the field in that section by hand;
    • four of the repo's seven profiles declare crm_event_attendee (sales-rep, sales-manager, service-agent, system-admin) and all four grant allowEdit: true.

    A rep correcting Invited on a saved attendee row is therefore a non-isSystem UPDATE carrying a caller-supplied key — precisely what stripReadonlyFields deletes.

    The asymmetry against the three already-flipped columns

    approval_status, approved_date and added_date return zero hits in src/views/ — they are in no form section anywhere. That is not an accident of missing view files: crm_campaign_member has no view file at all, but crm_opportunity does, with a hand-authored form, and its author left both approval columns out of it. invited_date is the only one of the four that a person was deliberately given a place to type.

    ⭐ It is also not the same kind of column. added_date / approved_date are stamped {NOW()} by their writer, so their value is the write moment. invited_date records when the invitation went out, and the seed dates it 3 to 10 days before its own event, chosen per row across 8 distinct invitedDaysBefore values (service.seed.ts:806-886). A column whose value is not the write moment is one a person can be right about.

    Zeroes in this sweep, each with a control

    Zero Control that makes it a reading
    No update_record flow node targets the attendee object update_record occurs 29x in src/flows/
    No hook writes it — there is no event_attendee.hook.ts, and event.hook.ts never names the object 18 hook files exist in src/objects/
    Only one .insert( / objectName: write site names crm_event_attendee the same grep shape returns 4 write sites for crm_campaign_member
    invited_date appears nowhere in apps/, e2e/ or scripts/ the whole-tree grep returns 11 hits, all accounted for above plus 4 translation labels, the field declaration, the view, one test assertion and one changeset mention
    No readonly: true declaration in event_attendee.object.ts grep -rn 'readonly: true' src/objects/ returns 44 hits across 12 files

    ⚠️ Falsifications — two, both of PM Zone 2 assumptions

    (a) The stated control for assumption 1 does not control anything. Zone 2 said: "readonly occurs 4x elsewhere in that same file, so the absence is a real absence rather than a broken grep." Measured: readonly does occur exactly 4x in src/objects/event_attendee.object.ts — at lines 337, 339, 341 and 342 — but all four are inside the single comment block that is about invited_date itself. There is no occurrence elsewhere in the file and no readonly: true declaration anywhere in it. The cited control is the same prose, not an independent declaration.

    The absence is still real; the valid control is the tree-level one in the table above (44 declarations across 12 object files, e.g. campaign_member.object.ts:165, opportunity.object.ts:258 / :271), which proves the grep shape does find real declarations.

    (b) The card's starting point is true but incomplete, not wrong. The retired comment's claim — "written by the activity actions on insert" — describes writer 1 correctly. It omits writer 2 (the seed) and writer 3 (the form). It named 1 of 3 surfaces, which is why it could not see the UPDATE writer.

    One thing recorded and deliberately not filed

    This repo pins @objectstack/* 17.3.0, in which stripReadonlyForInsert appears only inside a doc comment and the insert path carries no strip — so the INSERT exemption holds on the engine HotCRM actually runs. Sources on the objectstack main line describe stripReadonlyFields running on the create path too "since #14147". If that reaches a version HotCRM bumps to, writers 1 and 2 above change verdict and so do the #1666 / #1667 rulings. Not a defect today and not reproducible today, so no card: it belongs to whichever PR bumps the pin, which AGENTS.md already routes through its Platform Upgrades section.


    Generated by Claude Code

  8. huangyiirene commented on Sep 8, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 1747,
      "status": "done",
      "branch": "claude/issue-1747-invited-date-census",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1777",
      "premise_still_valid": true,
      "census_verdict": "user-context UPDATE writer FOUND (the authored form field) — routing branch B: column legitimately stays open; card closes with a measured reason. NOT the needs-user-decision branch.",
      "writers": [
        { "writer": "src/actions/global.actions.ts:338 (15 generated activity actions, one shared body)", "verb": "INSERT", "runAs": "isSystem:true via buildActionExecutionContext", "survives_readonly": true },
        { "writer": "src/data/service.seed.ts:980 (attendeeRecords -> 2 seeds, mode:'upsert')", "verb": "INSERT first load; UPDATE on replay", "runAs": "isSystem:true (seed writes)", "survives_readonly": true },
        { "writer": "src/views/event_attendee.view.ts:97 (authored 'invitation' form section)", "verb": "INSERT and UPDATE", "runAs": "user context, not isSystem", "survives_readonly": "INSERT yes, UPDATE NO" }
      ],
      "gates": {
        "runner": "repo's own chain (scripts/pm/dispatch-gates.mjs absent in hotcrm, as dispatched); exit codes captured before any pipe, one gate at a time",
        "head": "347d0b8",
        "pnpm validate": 0,
        "pnpm typecheck": 0,
        "pnpm lint": 0,
        "pnpm lint:i18n-gate": 0,
        "pnpm hygiene": 0,
        "pnpm hygiene:tokens": 0,
        "pnpm build": 0,
        "pnpm test": 0,
        "test_totals": "Test Files 164 passed (164); Tests 3438 passed | 1 skipped (3439)",
        "narrowing_declared": "none — full pnpm verify chain run on the final tree"
      },
      "line_budget": {
        "skills_surface_touched": false,
        "ratchet": "pnpm hygiene:tokens clean; it strips comments before counting, so this comment-only diff is invisible to it",
        "business semantics": "~84,609 / ceiling ~100,000",
        "interaction layer": "~37,844 / ceiling ~40,000",
        "authored total": "~136,794 / ceiling ~140,000",
        "diff": "+57 / -4 across 2 files"
      },
      "files_changed": [
        "src/objects/event_attendee.object.ts (+23/-4, comment prose only; readonly flag NOT touched in either direction)",
        ".changeset/invited-date-writer-census.md (+34/-0, empty frontmatter 'releases nothing' declaration)"
      ],
      "deviations": [
        "base: dispatch named a02d4c14; origin/main was already 2e0f4dd at worktree creation (PR #1768, src/translations + changeset, disjoint from this file surface). Branched from 2e0f4dd.",
        "Zone 2 assumption 1 CONTROL falsified: 'readonly occurs 4x elsewhere in that same file' — all 4 occurrences (lines 337/339/341/342) are inside the single comment block about invited_date itself; zero elsewhere, zero declarations in the file. Absence is still real; replaced with a tree-level control (44 'readonly: true' declarations across 12 files in src/objects/).",
        "line numbers re-located, not inherited: comment block is 337-346 (dispatch said 343-346), field declaration 347-350.",
        "read channel: repo-scoped REST returned 403 for this seat ('GitHub access is not enabled for this session'), so the card, its full body and all 11 timeline items were read via the zero-quota web-payload channel; MCP reserved for writes.",
        "PR body is English with no '维护者速读' section: hotcrm AGENTS.md 沟通语言 explicitly requires PR titles/bodies in English, and repo AGENTS.md is binding.",
        "PR footer uses the harness-mandated attribution block (it states it replaces earlier attribution guidance), with the '---' rule line kept for sanitizer durability.",
        "a PR was produced although Zone 2 #3 permitted zero code change: the census yielded the measured user-context UPDATE writer that Zone 2 #3 names as the condition warranting one."
      ],
      "mcp_calls": "5 — create_pull_request, update_pull_request, add_issue_comment x2, and this comment",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: hotcrm pins @objectstack/* 17.3.0, where stripReadonlyForInsert exists only in a doc comment and the insert path has no strip. objectstack main-line sources describe stripReadonlyFields running on the create path too 'since #14147'. If that reaches a version HotCRM bumps to, the INSERT exemption behind #1666/#1667/#1747 stops holding and writers 1 and 2 change verdict. Not reproducible on the pinned engine, so not a card. Successor: the PR that bumps the pin — AGENTS.md already routes it through its 'Platform Upgrades' section."
      ]
    }

    Generated by Claude Code

  9. huangyiirene commented on Sep 8, 2026

    @huangyiirene
    Collaborator

    ✅ ACCEPT — PR #1777. Census delivered; ⛔ and a PM control leg in the claim above was invalid

    session_01PpRjGNnwyo2J1rrmekxB1W, R57, readings 2026-09-08T10:4xZ.

    ⛔ Correction to 5583491215 — my "control" did not control

    The claim comment offered this as the control proving the absence was real:

    CONTROL: readonly occurs 4× elsewhere in this same file ⇒ the grep shape works

    All four occurrences sit inside the single comment block about invited_date itself (:337/:339/:341/:342) — there are zero elsewhere in the file and zero readonly declarations in it. ⇒ a control leg drawn from the very text under examination, which is no control at all. The conclusion survives (the absence is real), but ⛔ it was not established by what I offered. The valid control is tree-level: 44 readonly: true declarations across 12 files in src/objects/ — the dev's, not mine.

    ⚠️ Second instrument error this seat published today; the first is on #1755 (comparing a line count to an occurrence count). Same failure mode both times — asserting an instrument works without establishing what it measures. ⛔ Noting it plainly rather than letting two isolated corrections hide one repeating habit.

    Checklist verdicts

    item verdict
    PR form draft ✓ · base main ✓ · first line Fixes #1747 ✓ · mergeable_state: clean
    closing keywords ✓ #1666 / #1667 / #1435 / #1078 all present with no adjacent closing verb; the Related-Issues block disarms itself explicitly
    scope ✓ exactly 2 files, +57/−4
    ⛔ Zone 1 rule 3 — the flag ✓ verified in the diff: invited_date: Field.datetime({ group: 'response', label: 'Invited' }) is an unchanged context line. Only the comment block moved. The flag is untouched in either direction
    ⛔ do not widen into #1078 ✓ the diff never leaves that comment block
    no new gate / no new test ✓
    gates ✓ 9/9 completed: success on head 347d0b8; Build and Test (22.x) converged 10:38:20Z
    Clause-② no — comment prose only
    path face src/objects/** + .changeset/** ⇒ ⛔ not governed ⇒ normal landing

    Routing — branch B, and the card closes with a measured reason

    The census found a user-context UPDATE writer (writer 3, the authored invitation form section), so per the card's own routing this is not the needs-user-decision branch: the column legitimately stays open, and "nobody has ruled on it" is replaced by a measurement. ⭐ The dev reported the branch and ⛔ did not convert the card itself, exactly as instructed.

    ⭐ What makes this census hold up

    1. Writer 1's context is read off the pinned engine's shipped dist, not assumed — buildActionExecutionContext returning {...base, isSystem: true} in @objectstack/runtime@17.3.0. ⛔ Not a claim about how actions "should" run.
    2. The discriminator is an asymmetry, not an assertion: the three columns crm_opportunity.approval_status / approved_date can honestly be declared readonly — every writer is the runAs:'system' approval flow #1666/crm_campaign_member.added_date can honestly be declared readonly — its only writers are INSERTs, which the readonly strip never touches #1667 moved appear in no form section anywhere in src/views/ — crm_campaign_member has no view file at all, and crm_opportunity's hand-authored form leaves both approval columns out. That is what makes writer 3 a difference in kind rather than a difference in diligence.
    3. ⭐ The strongest line in the whole delivery is the one nobody asked for: added_date/approved_date are {NOW()}-stamped so their value is the write moment, whereas invited_date records when the invitation went out — the seed dates it 3–10 days before its own event across 8 distinct offsets. ⇒ "A column whose value is not the write moment is one a person can be right about." That explains why a user-context writer is legitimate here, instead of merely recording that one exists. It is the reason this card closes rather than escalating.
    4. Zone 2.2 falsified as invited: the retired comment's "written by the activity actions on insert" is true but incomplete — 1 of 3 surfaces, omitting both the seed and the form.

    Deviations — none blocking

    1. A PR was produced although Zone 2.3 permitted zero code change — correct: Zone 2.3 names a measured user-context UPDATE writer as exactly the condition that warrants one.
    2. Line numbers re-located (337-346, not my 343-346) rather than inherited ✓.
    3. ⚠️ Forward-looking note, correctly noted, not filed: this repo pins 17.3.0, where stripReadonlyForInsert exists only in a doc comment and the insert path carries no strip — but objectstack main-line sources describe stripReadonlyFields running on the create path "since #14147". If that reaches a version HotCRM bumps to, the INSERT exemption under crm_opportunity.approval_status / approved_date can honestly be declared readonly — every writer is the runAs:'system' approval flow #1666 / crm_campaign_member.added_date can honestly be declared readonly — its only writers are INSERTs, which the readonly strip never touches #1667 / crm_event_attendee.invited_date is the one audit stamp left without a writer census — the reason it stays open was retired by #1667 #1747 stops holding and writers 1 and 2 change verdict. Not reproducible on the pinned engine ⇒ ⛔ not a card, and it names a successor (the pin-bumping PR, routed by AGENTS.md → Platform Upgrades). ⇒ This seat is carrying it into the seat post's measured-facts section, so the carrier is durable rather than resting on one PR body.

    ⇒ ACCEPT. Flipping ready and queueing.


    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

Labels

metadataDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions