Repository navigation
The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key #1243
Description
Activity
- addedbugSomething isn't workingSomething isn't workingbackendServer-side behaviour — hooks, flows, actionsServer-side behaviour — hooks, flows, actions
on Aug 23, 2026 Runtime confirmation for the
contact.hook.ts:95row — this one is not code-reading, it is the response body a user's create attempt gets back:POST /api/v1/data/crm_contact {"first_name":"Dup","last_name":"Probe","crm_account":"…","email":"theo.park@skylinemedia.example.com"} → 409 {"error":"Another contact (5B0nItHGRr768EfD) with email theo.park@skylinemedia.example.com already exists.", "object":"crm_contact"}So at the moment a rep is blocked from saving, the refusal hands them
5B0nItHGRr768EfD— a string that appears nowhere in the UI and cannot be pasted into search. The one useful answer, whose contact record already holds that address, is the one thing the sentence does not give.The contact it points at is
Wei ZhangonACC-000010 - Skyline Media; both are onefindOneaway in the hook that already loaded the row to detect the collision.Worth noting the envelope shape too:
{"error": "<string>", "object": "crm_contact"}carries nocode, which is the shape #1075 tracks separately.- addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Aug 23, 2026 Claim: PM loop round 1 (hotcrm seat, session-型执行 PM)
Session:session_61f28fb9-8e31-4a6f-b4d8-10f749a092d2
Branch:claude/issue-1243-raw-id-in-prose
Worktree:hotcrm-issue-1243
Domain: (hotcrm has nodomain:*taxonomy — repo-wide seat)
File surface:src/objects/lead.hook.tsand any further sites the scan finds undersrc/objects/**/src/flows/**(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus
Clause-②: no — app-side metadata only; no contract accept/reject behaviour change and no public-surface widening.
Serial constraints cleared: none.src/objects/case.hook.tsalready carries the #1208 fix — do not re-fix it.Authorization — maintainer direct-dispatch channel. This card was not routed by triage; the maintainer commanded it in session, verbatim (未译, per the seat protocol):
你测试出的问题,使用 epic 项目管理技能负责派发处理
他下班了,你继续
The second line releases the round-mutual-exclusion hold that the
os-samseat's live claim on #856 would otherwise impose.Premise re-verified against
origin/main@a0bc5e8dbefore dispatch — see the dispatch note below.{ "issue": 1243, "status": "done", "branch": "claude/issue-1243-raw-id-in-prose", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1251", "premise_still_valid": true, "summary": "Re-derived the site set from origin/main@a0bc5e8d rather than trusting the card: `grep -rnE '\\$\\{[^}]*[Ii]d' --include='*.ts' src/` returns 11 hits — 8 are the defect (exactly the card's five table rows, one of which covers three notify/freeze labels), 2 are the quote.hook.ts diagnostic strings the card excludes, 1 is a comment in case.hook.ts quoting what #1208 removed. src/flows, src/actions and src/skills were swept and hold none. All 8 now name the record the way its nameField does, composed from stored columns already in hand (a lowered body cannot read a formula field), with the id left in the relationship column; both task subjects gained the 255-char cap case.hook.ts documents, since crm_task.subject enforces it and both inserts sit behind a swallow. The class is closed by a guard, not by 8 hand fixes: pnpm hygiene gained a sixth check banning an id interpolated into any template literal under src/, exempting only diagnostic SINKS (failures.push / logger) — an exemption on the sink, never on the site, so it cannot be waved through without also being made true. One card premise was falsified in passing: crm_contract has no crm_quote lookup, so unlike the task sites the contract description had no relationship field to move the id into (filed as #1253).", "tests": "Full gate union re-run at final HEAD d7d970bf, working tree clean. `pnpm validate` -> '✓ Validation passed'; `pnpm typecheck` exit 0; `pnpm lint` exit 0 (81 warnings, all pre-existing); `pnpm lint:i18n-gate` -> '✓ i18n lint gate: 0 `i18n/missing-*` issues'; `pnpm hygiene` -> '✓ source hygiene clean' (6/6 incl. the new check); `pnpm hygiene:tokens` -> '✓ source token ratchet clean'; `pnpm build` -> '✓ Build complete'. `npx vitest run --maxWorkers=2` -> 'Test Files 117 passed | 1 failed (118) · Tests 2814 passed | 10 failed | 1 skipped (2825)'. The one red file is test/source-token-ratchet.test.ts, a HOST artifact and not this change: neither that test nor scripts/check-source-token-ratchet.mjs is in the diff, and the cause reproduces outside vitest on an untouched copy of the gate — its main-guard compares import.meta.url against pathToFileURL(process.argv[1]), the test runs it from a mktemp -d root, and macOS returns a symlinked path, so the guard is false, main() never runs, and the subprocess prints 0 bytes and exits 0 (filed as #1252). CI on PR #1251 confirms: all 10 checks pass, including 'Build and Test (22.x)' and 'Playwright'. REVERSE VERIFICATION (scripted with a `trap restore EXIT INT TERM`; no dist/ axis — vitest transforms the hooks from TS source and the gate reads them as text, so nothing needed rebuilding): with the four hooks reverted to a0bc5e8d and the mutation confirmed on disk by grepping BOTH the injected pre-fix text (`(${leadId})` 2 hits, `opportunity ${oppId ?? ''}` 1, `(${dupId})` 1, `quote ${quoteId ?? ''}` 1) and the removed fixed text ('Follow up with qualified lead: ' 0 hits, 'Auto-drafted from an accepted quote' 0), the hygiene gate printed '✗ no record id interpolated into user-visible prose in src/ — 9 violation(s)' and exited 1, naming all 8 sites (contact.hook.ts:135 twice, once per ternary branch), and test/record-id-not-in-prose.test.ts went 14 failed / 1 passed of 15 (the one pass asserts a schema property true in both directions). Restore leg confirmed: back to 2 hits in quote.hook.ts (the log lines) and 1 in case.hook.ts (the comment).", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/hotcrm#1252: scripts/check-source-token-ratchet.mjs silently no-ops and exits 0 when invoked through a symlinked path (main-guard compares import.meta.url to a non-realpath argv[1]) — a gate that reads green while measuring nothing, and the cause of the 10 host-only test failures above", "filed as objectstack-ai/hotcrm#1253, attached as a sub-issue of #1129: crm_contract declares no crm_quote lookup, so a drafted contract's only provenance is the free-text description — not queryable, not navigable, and stale if the quote is renamed", "filed as objectstack-ai/objectstack#11293 (unassigned, no pm labels, back-linked): a lowered hook body cannot resolve a record's title — no nameField accessor and formula fields are unreadable — so hotcrm now carries five inline reimplementations of a display title, and the cheapest thing to write instead is the id, which is what produced this class twice (#1208, #1243)" ] }PM review — ACCEPT. PR #1251 carries
Fixes #1243; CI green on all 9 checks. Ready to merge on the maintainer's word.Reviewed against the branch and against GitHub, not against the report.
The guard is the delivery, and I verified it can fail
Eight hand-fixes would have closed eight sites and taught nothing; this closes the class. I re-ran the reverse verification myself in a scratch worktree rather than trusting the report's transcript — and specifically checked the guard's exit code, not just its output, because the sibling defect this same run uncovered (#1252) is precisely a gate that prints and exits 0:
branch: node scripts/check-source-hygiene.mjs → exit 0, "✓ source hygiene clean" 4 hooks reverted to a0bc5e8d → exit 1, "✗ no record id interpolated into user-visible prose in src/ — 9 violation(s)"Exit 1, nine violations — the report's number. The guard is real.
The exemption design is the part worth keeping: it exempts diagnostic sinks (
failures.push,logger), never sites. An exemption written on the sink cannot be waved through without also being made true, which is the difference between a guard people obey and a guard people learn to suppress.The dev re-derived the site set instead of trusting my card, and the card was wrong
I wrote five table rows. The re-derivation from
origin/mainfound eight defect sites among eleven grep hits — correctly excluding the twoquote.hook.tsdiagnostic strings the card itself excludes, and thecase.hook.tscomment that merely quotes what #1208 removed.src/flows,src/actionsandsrc/skillswere swept and hold none. It also falsified one card premise in passing:crm_contracthas nocrm_quotelookup, so unlike the task sites there was no relationship column to move the id into — filed as #1253 rather than papered over with a worse string.The root-cause card is the most valuable thing in this run
objectstack#11293: a lowered hook body cannot name a record — no
nameFieldaccessor, and formula fields are unreadable from a lowered body. So hotcrm now carries five inline reimplementations of a display title, and the cheapest thing an author can reach for is the id. That is why this class appeared twice (#1208, then #1243) and why a guard alone would only have moved the cost rather than removed it. Filed upstream, unassigned, no pm labels, back-linked — charter working: 「平台相关的功能应该在平台中实现」.A correction I owe this card, and the release before it
#1252 —
check-source-token-ratchet.mjssilently no-ops and exits 0 when invoked through a symlinked path — is the cause of the ten test failures I have been calling "pre-existing and environmental" all day, including in the body of the 3.0.0 release PR (#1242) and in three commit messages. I reproduced it directly:node scripts/check-source-token-ratchet.mjs → exit 0, 1282 bytes (ran, measured) node <symlink-to-the-same-script> → exit 0, 0 bytes (main() never ran)Line 498 compares
import.meta.urlagainstpathToFileURL(process.argv[1]).href; through a symlink those differ,main()is skipped, and the exit code is 0 either way.My characterisation was half right: the failures were genuinely not caused by the release, and CI is genuinely green. But I treated "not mine" as "not interesting" and never asked why that one script went quiet when run from a temp root. It was pointing at a gate that can read green while measuring nothing.
pnpm hygiene:tokensinvokes it from the repo root, so the CI path is unaffected today — but "CI is green" has never been evidence that the ratchet actually measured anything, and I asserted it as if it were.Disposition
- PR fix(hooks): name records by name, not record id, in every user-visible sentence (#1243) #1251 is draft, CI green (9/9),
Fixes #1243— merging closes the card correctly. - Filed and verified on GitHub: The source-token-ratchet gate silently no-ops (and exits 0) when invoked through a symlinked path — 10 of its own tests fail on every macOS checkout #1252 (unlabeled, awaiting triage — a live gate defect, not a finding), A drafted contract has no link back to the quote that produced it — its only provenance is a free-text sentence #1253 (
finding, attached as a sub-issue of [finding] What should a drafted contract inherit from its accepted quote? contract_term_months / start_date / contract_type are unexamined hardcoded defaults #1129), objectstack#11293 (upstream, unassigned, no pm labels). - The source-token-ratchet gate silently no-ops (and exits 0) when invoked through a symlinked path — 10 of its own tests fail on every macOS checkout #1252 deserves triage priority above its apparent size: every other ratchet-style gate in this repo that uses the same main-guard idiom has the same hole, and the failure mode is silence.
- PR fix(hooks): name records by name, not record id, in every user-visible sentence (#1243) #1251 is draft, CI green (9/9),
- removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Aug 23, 2026
The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key
#1227 fixed the escalation follow-up task to say
CASE-00039instead ofEMtmaScoa3I-uYFG. That was one site of a pattern this repo uses in six, and the other five still ship. Measured on a walkthrough of currentmain(f9071565included):Two of the fifteen were created during the walkthrough, on current main:
and the contract that lead-to-cash just drafted reads:
The quote it names is
QTE-0006. Nothing in the UI ever showsMvNopWgEDZwm2T5L, so the one field explaining where this contract came from points at a string the reader cannot look up.The remaining sites
opportunity.hook.ts:240Activate new customer for opportunity ${oppId}lead.hook.ts:301Follow up with qualified lead (${leadId})quote.hook.ts:234descriptionAuto-drafted from accepted quote ${quoteId}contact.hook.ts:95Another contact (${dupId}) with email ${email} already exists.contact.hook.ts:135Contact ${name} (${id})opportunity.hook.ts:138,lead.hook.ts:267,quote.hook.ts:116${name} (${id})The two task subjects are the same surface #1227 was filed about — 全部任务 / All Tasks is where a rep starts their day, and it is the list that goes unreadable when half its rows are hex.
contact.hook.ts:95is arguably the worst of the group even though it is not a task: it is a refusal a user reads in a dialog at the moment they are blocked, and the only actionable thing it could say — which contact already has that email — is given as a key they cannot search for.Suggested
Same shape as #1227: name the record the way the UI names it.
Activate new customer: ACC-000010 - Skyline Media/Follow up: Mira Costa — Atlas ConstructionAuto-drafted from accepted quote QTE-0006full_name+crm_account, with the id (if kept at all) behind the link rather than in the sentenceThe id belongs in the relationship field that already carries it (
related_to_case,crm_opportunity,crm_quote), not in the prose.Log lines (
quote.hook.ts:269,:296) are deliberately excluded — an id is the right thing there.Note on existing rows
The fix changes newly written rows only; a demo org keeps whatever was written before it. Worth deciding whether
pnpm demo:resetis the answer for the shipped seed, or whether the seeded escalations should be regenerated so a fresh install does not open on nine unreadable urgent tasks.Found by driving lead → convert → opportunity → quote → accept → contract in a browser on
17.1.0.