Repository navigation
test: run the hook, flow and action suites on @objectstack/verify and retire the five hand-built harnesses - #2013
Conversation
The in-process handle (`bootStack` / `bootStackOnce`, `hooks.run`, `flows.run`, `actions.run`, `seed`, `rows`, `metadata`, `tenancy()`) is the door the suites will reach the real engine through. It resolved only as a transitive dependency of the CLI until now. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
…andle `metadata-fixtures.ts` mixed two kinds of value. The authored ones — the composed stack's collections, the locale packs, a tree walker — are views of `composed-stack.ts` and now live there or in the suite that reads them. The runtime ones were stand-ins for the platform: a platform-object roster assembled from six package exports plus a hand-listed `AUDIT_PROVISIONED`, and a hand-listed set of system columns for `fieldsOf`. Those now come from the booted app's own registry (`metadata.objects()` / `metadata.object()`), through `@objectstack/verify`'s `bootStackOnce` over the shipped artifact. `test/helpers/verify-stack.ts` is the one boot every suite shares: a single options constant so `bootStackOnce`'s identity memo can share it, and the four capability plugins this app's `requires[]` resolves to under `objectstack serve` that the lean boot was measured to omit (record-change triggers, approvals, messaging, audit). `@objectstack/plugin-audit` is declared at the exact pin because that file imports it. Measured delta of the platform-object set (72 roster names → 73 registry names): the registry adds the RBAC objects the security plugin registers (`sys_permission_set`, `sys_position`, …) that the roster missed, and lacks the storage/job/email objects only the always-on slate mounts. No reference in today's metadata changes verdict; the 18 suites pass unchanged. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
… kernel Six suites assembled their own kernel — memory datasource, metadata, objectql, hand-registered identity objects, AppPlugin with skipSeedData, security, sharing — and, because that assembly mounted no auth plugin, a fake `tenancy` service from `tenancy-probe.ts` so the declared sharing rules would seed. They now boot the shipped artifact with `@objectstack/verify`'s `bootStack` (the real AuthPlugin's tenancy service, posture `single`), sign their people up, give them positions and permission sets as system writes (`signUpPerson`), write as them with `hooks.run`, read as them with `rows`, and read each person's context from `contextFor`. Two expectations moved with the means, the business fact unchanged: - admin standing is read off the resolved context's `posture` (`PLATFORM_ADMIN` / `MEMBER`), the field the platform's resolver sets; `hasPlatformAdminGrant` was `buildContextForUser`'s. - the boot replays the app's seed rows, so reach readings are taken over each fixture's own rows (seeded ownerless open cases and US accounts are reached by the same rules and are not the pinned population). Two writes have no door on the 17.7.0 handle and keep one local path in `verify-stack.ts`, the engine's own objectql on the verify-booted kernel: a system-context UPDATE (`seed` only inserts) and a GUEST insert (no user, not system; an unauthenticated POST /api/v1/data/crm_case answers 401). `sharing-posture-declaration.test.ts` is deleted with the probe: it asserted that hand-built kernels mount `tenancyProbe` before `SharingServicePlugin`, its remedy text named the deleted helper, and with every boot going through the platform its population is empty by construction (measured: 0 mounters, its sentinel red). Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
`action-sandbox.ts` ran each action body and lowered hook body in QuickJS over a stub ObjectQL that copied measured kernel rules. Every importer now reaches the real engine of the shipped app booted through `@objectstack/verify`: actions through `actions.run` (dispatcher gate, param contract, subject load under the caller's scope, then the body in QuickJS), hooks inside real writes through `hooks.run`, rows read back through `rows`. The suite that proved the stub (`action-sandbox.test.ts`) also carried this app's business facts; those move, ported, to `script-bodies.test.ts` (every script action runs, mass_update_stage, clone / mark_primary / campaign enrolment / mark_responded / send_email, the #678 actor name, the price fill, the territory derivation). What it held that only proved the stub or the platform runner is deleted with it, as are `harness-lookup-shape` and `hook-write-shape`, the named proof suites that import the stub. The "every hook still lowers" sweep is the platform's: `os lint --strict` refuses a non-lowerable hook as `hook-body/not-lowerable` (measured by ablation on a scratch copy: control exit 0, mutant exit 1). Body-text checks call the platform's public `@objectstack/cli/hook-body` extractor directly. Files that used this stub and `hook-harness` for the same scenario are ported whole here (the quote-accepted trio, record-id-not-in-prose, refusal-envelope, global-actions, do-not-call-enforcement, escalation-task-subject). Paths kept in `verify-stack.ts` for what the 17.7.0 handle has no door for, each reported upstream: a spy-based recorder of the writes the engine receives (what a hook handed over; when an async hook finished; a staged refusal), and the shipped-body runner the refusal-envelope suite pins the production envelope on (platform extractor + platform runner over the real engine). `@objectstack/plugin-email` is mounted (always-on `email`, which `send_email` needs) and declared at the exact pin. Expectations the real engine moved, the fact unchanged, each stated in its test: a rep's quote acceptance cannot draft a contract (no crm_contract create; the hook writes as the caller), so the draft suites accept as a sales manager and the rep case is reported; a contact-less quote can no longer be accepted (#1017), so those pre-images are pinned as the refusal; required name/subject/number columns make several "blank half" branches unreachable, pinned as the refusal that makes them so; the dispatcher refuses undeclared params and a bare value for a multiple lookup before any body runs; a fresh database boots value-shape strict, so warn-first is asked for with OS_ALLOW_LAX_VALUE_SHAPES. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
`flow-harness.ts` ran the real AutomationEngine over an array-backed stand-in data engine (its own `$gt`/`$lt`, its own hook dispatch order, a stub messaging service, no permission check). Every importer now runs the shipped app booted through `@objectstack/verify`: screen flows through `flows.run` / `flows.resume` as a real persona, record-triggered flows off real writes (a person's `hooks.run`, a guest web-to-case/web-to-lead insert, or a system write), sweeps through the trigger door as the admin, and effects read back off the engine: rows, the run history (`sys_automation_run` via `flowRuns`), the notification outbox (`sys_notification_delivery` via `notificationsTo`) and the HTTP outbox (`sys_http_delivery`). Flow variants a fact needs (a tampered gate, a dead gate, a runAs-less twin) are registered through the platform's own authoring door (`POST /automation`) and removed with `DELETE`; approvals are decided through `POST /approvals/requests/:id/approve|reject`. `flow-harness-declared-columns.test.ts` goes with the stand-in it proved. Files that used this stand-in together with `hook-harness` are ported whole here (flow-scheduled, knowledge-deflection, account-name-normalized- match, the lead-conversion, qualification and status-change approval gates). `readonly-write-semantics` and `audit-stamp-readonly` only took the silent logger from it; they assemble their own real ObjectQL and keep it. Sweeps process every match in the database, seed rows included, so each reading is taken over the case's own rows (by id, by the notification's action URL), or as a delta over a database settled by a first sweep. Expectations the real engine moved, each stated beside its test: - the start-condition truth tables run off real writes where a write can produce the shape, and through the engine (`runRecordFlow`) only where none can (an ownerless contact, a deal born pending, a vanished case); - runAs-less variants of case_escalation, opportunity_approval and both approval gates are refused at the authoring door (422 `flow-update-readonly-field`) instead of failing at run time; - approvals now really open (the harness lacked the approvals plugin): runs park `paused` with a `sys_approval_request`; the "flow's own write" cases run end to end through a real decision; - a user write carrying a readonly verdict is stripped and judged by the stored one (qualification INPUT-FIRST case flips to RECORD_LOCKED); - an update addressed to a vanished case is refused (`Record … not found`), so that run fails at the escalation write, still notifying no one; - `acted` counts tasks; notifications are `unmeasured`; - no NULL amount, email-less lead/contact or blank owner from a person can be written; those shapes are pinned as the refusal (blank and numeric owners stay, as system writes); - a bulk update binds each row's pre-image (no `previous: null`); - a lead with a NULL match key converts onto an UNRELATED key-less account — the documented stop does not hold on either datasource; pinned as the measured defect and reported. Local paths kept in `verify-stack.ts` for what the 17.7.0 handle has no door for, each reported upstream: a predicate (bulk) update, and running a record-triggered flow on a record the engine does not hold. Kernel services the handle does not front are read directly where a fact needs them: the automation service's own condition evaluator (`conditionHolds`), and the sharing service's rule reconcile. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
`hook-harness.ts` called each hook handler directly, with a hand-built ctx and a `ctx.api` over plain arrays. Every importer now writes through the shipped app booted by `@objectstack/verify`: the persona each case is about writes through the engine's write door (`hooks.run`), prior states are put in place by the system (`seed`, the one system-update path), anonymous web submissions go through the form door's own execution context, and what is asserted is the row the engine stored or the refusal it raised. Async hooks are waited for; a claim that a hook wrote nothing is read off the writes the engine received. `guestInsert` now writes under the context the platform's anonymous form door uses (the form's one-object grant, `guest_portal`, `anonymous`) rather than an empty context no door produces. Under it the intake round-robins and the duplicate lookup cannot read their pools, so the two guest-facing cases that had read otherwise now pin the stored truth; `runRecordFlow` also covers the user-less trigger a seeded record cannot fire. Branches no write can reach (an unknown select value, an absent `ctx.api`, a key-less row the schema requires) are pinned as the engine's refusal, or re-aimed at the writer that does reach them. Where the real write measured a defect the stand-in hid, the case is pinned as a measured defect beside the same behaviour shown through a writer that holds the rights. Deleted with the stand-in: `hook-input-shape` and `hook-query-predicate`, the named suites that proved it and the engine shapes it modelled. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
AGENTS.md cited `test/hook-query-predicate.test.ts` for the `where`/`filter` engine behaviour and `test/action-sandbox.test.ts` for the lowering check; both suites are gone with the stand-ins they proved. The lowering check is `pnpm lint` (`hook-body/not-lowerable`), the engine behaviour is recorded in `_hook-api.ts`, and the `mass_update_stage` body runs in `test/script-bodies.test.ts`. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
…them `runtime-coverage` requires every registered hook and flow name to appear in code of a runtime test file. The ports drove `opportunity_line_item_price_fill`, `billing_handoff_contract_activated` and the two per-person enrolment subflows through real writes and runs without spelling their names: the price fill's describe now carries the hook name, the contract hand-off case reads the flow's own run history by name, and the enrolment suite asserts the screen's two subflows by name and that both are registered on the booted app. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
| } | ||
| }; | ||
|
|
||
| const nodeStatus = (summary: Rec | undefined, id: string) => |
A test's title must say what its body asserts. Porting onto the verify handle kept some base titles verbatim while the real engine turned the asserted outcome around (allowed to refused, written to refused, omitted to written). Each such case is retitled to what it now asserts. Three are defects, pinned in the measured-defect form with a rewrite-me message: - a customer account whose deals are all closed still cannot be deleted (the guard says "Close or reassign it first"; closing is not enough); - an account whose contracts are all draft, expired or terminated still cannot be deleted (cascade order); - a contact whose only contract is expired still cannot be deleted (the guard says "Close or reassign those records first"). The rest are reworded to the outcome the body now asserts, mostly an input the engine refuses before the hook could see it. No matcher changes (the three defect pins' failure messages now carry the rewrite-me text); the contact case is split in two so each half has a true title, and the whitespace-only fold row moves to its own table. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #1595 (body, ruling 6051426954, claim 6055790691, os-dev-reports 6060172268 and 6060702334), PR #2013 body and file list, ① Derived judgmentsA. The governed
B. Deleted or rewritten tests vs "Zero tests deleted that pin a business fact" — RIGHT.
C. The 13 D. E. Dependency additions vs the pin rule — RIGHT. F. Scope and gates. No ② Semver levelhotcrm is ③ Boundary flagsDev report 6060172268 — two
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读
改了什么
为什么改 风险与代价(含回滚)
这次暴露出的产品缺陷(已立卡,不在本 PR 修)
席位意见
你要做的(一个动作) Generated by Claude Code |
Fixes #1595
Clause-②: no
The test suites stop running on hand-built stand-ins. Every hook, flow and action suite now boots the shipped app (
objectstack.config, both packages) through@objectstack/verify17.7.0 and drives it through the platform's own doors. The five stand-ins undertest/helpers/are deleted, along with the seven suites that only proved them. Each ported test pins the same business fact as before. Where a real write cannot reproduce the old setup, the test now pins what the engine actually does, and the change is listed below.Before: a hook test called
hook.handler(makeCtx(...))with actx.apibacked by plain arrays. A flow test ranAutomationEngineover an array "driver" with a stub messaging service. An action test ran QuickJS over a stub ObjectQL.After: a person writes through
hooks.run, flows run throughflows.run/flows.resume, actions run throughactions.run. Prior states are set withseedor a system update. Results are read withrows. Async hooks are waited for.Mechanism probe (assumption 1)
Probe run on branch base
99d290adunder the verify lock (VERDICT command-exit 0).bootStack(objectstack.config, { automation: true })booted in 3.35 s. It registered 32 flows, 42 hooks and 22 actions from both packages (app.objectstack.hotcrm.service,app.objectstack.hotcrm).hooks.runinsert:account_protectionstampedname_normalized/territory/billing_country, andopportunity_lifecyclestampedprobability10.case_sla_defaultsstampedpriority_rankandsla_due_date.case_escalation_stamp→{success:true}.campaign_lead_member_enrollwrote 1 member.schedule_followup→paused.{posture:'single', isolationActive:false}.The lean boot was missing five capabilities that this app's
requires[]resolve to underobjectstack serve: triggers, approvals, messaging, audit and email. These are mounted throughextraPluginsintest/helpers/verify-stack.ts(gap 1 below).Per-helper port
metadata-fixtures.ts(172)metadata.objects()/metadata.object()onbootStackOnce; authored collections moved tocomposed-stack.tse0a85770tenancy-probe.ts(86)bootStack(real AuthPlugin tenancy),signUpPerson→signUp+ system RBAC rows,hooks.run,rows,contextFor69a26060action-sandbox.ts(427)actions.run(dispatcher → QuickJS body → real engine),hooks.run,rows; body text via@objectstack/cli/hook-body49b229a3flow-harness.ts(720)flows.run/flows.resume,hooks.run/seed/rows,POST/DELETE /automationfor flow variants,sys_automation_runrun historyc593a86bhook-harness.ts(641)hooks.run/seed/rows, the form door's context for guests,recordEngineWritesfor writes a hook handed over13d8d742Follow-ups on the branch:
1f952f98(AGENTS.md and the developer guide: citations repointed off the deleted suites),330517ff(changeset),632787ab(runtime-coveragereads names: the ported price-fill hook, the contract hand-off flow and the two enrolment subflows are named again where they run). Base99d290ad.Deleted self-proofs:
flow-harness-declared-columns,harness-lookup-shape,hook-input-shape,hook-write-shape,hook-query-predicate,action-sandbox. Also deleted, not on the card's list:sharing-posture-declaration. It asserted that hand-built kernels mounttenancyProbebeforeSharingServicePlugin. After the port no suite mounts that probe, so it checks nothing (measured: 0 mounters, its own sentinel red). The business factsaction-sandbox.test.tscarried moved to the newtest/script-bodies.test.ts(29 cases).The five exemplars, asserted fact before → after
hooks-runtime-salesmakeCtx, store rows read backhooks.run(rep, manager for contracts/acceptance, admin for deletes); stored row or refusal. Same 89 facts. Changes: an unknown stage is refused by the engine (VALIDATION_FAILED) before the hook runs; the quote freeze names the quote by its display title; rep quote acceptance cannot draft a contract (finding F1), so acceptance runs as a manager; "all opps closed" account deletes are refused by the engine's RI rather than allowed (pinned as engine-not-guard); and "drafts/expired/terminated cascade with the account" is refused on the contacts (F8)flow-quoteflows.run+resumeas the rep on a system-seeded deal. #1206's property (both fields inexact before the fix) is re-measured at 34 %, because the 60 % ceiling oncrm_quoterefuses 70 %global-actionsactions.runon all activity targets, events, attendees,record_label,actor_nameread from stored rows. Two changes: a single picked contact is refused as a bare value by the dispatcher (a multiple lookup wants a list), and an unparseable start date is refused by the dispatcher before the body can write NaNsla-at-risk-live-workcase_sla_defaultsrun on a synthetic ctx, columns inserted into a one-object ObjectQL (memory + sqlite)hooks.runon the shipped app, on the sparse and the SQL datasource, and the shipped view filter runs throughrows. Identical: a resolved case storesis_closedfalse, the old spelling returns[live-critical, resolved-high], and the shipped filter returns exactly[live-critical]unassigned-case-triage-reachObjectKernel+tenancyProbe('single')+SharingServicePluginon memory and sqlitebootStackon both drivers, the agent signed up, writes ashooks.run, reads asrows({as}), shares read fromsys_record_share. Same assertions. Admin standing is now read from the resolved context'sposture(MEMBER) instead ofhasPlatformAdminGrant, and reach is measured over each fixture's own rows (the boot replays seed cases)Tests whose expectation changed (before → after; the business fact is unchanged unless the row says otherwise)
Every change is also explained in a comment beside the test.
Flows (
c593a86b)flow-quote: discount 70 % → 34 %, because the ceiling refuses 70 %. Deals are system-seeded because a rep-created deal of 100k or more is approval-locked (F2).flow-conversion: the converter is now a sales manager instead of a rep, because a rep's conversion into a new account is refused (F3). Lead statusworking→contacted, becauseworkingis not an option.flow-billing-handoff: deliveries are read from the real outboxsys_http_delivery(receiver stubbed 503). "Bulk update" is now a real predicate update: 17.7.0 binds each row's pre-image, so a predicate update intoclosed_wonhands off once and a bulk edit of a won deal hands off zero. The reverse check registers a flow clone throughPOST /automation. Statusactiveis now refused at the save.close_dateis the stored one.flow-campaign-enrollment: contacts and leads with no email cannot exist (email is required), so those fixtures are pinned as the refusal.flow-case-actions: claim — the flow's own payload carries noowner_id(the recorder shows it), and the stored owner is the caller. This replaces "owner still null" from the store that had no hooks.flow-escalation-ownerless-case: the verdict is read fromsys_automation_run. The blank-owner × on_create shapes cannot be written by a person, so they are pinned as the refusal. A record that vanished now fails atassign_senior_agentinstead of succeeding. 31 → 27 cases.flow-record-change: start conditions are exercised with real writes. Shapes that no write can produce run throughrunRecordFlow. The Record-change flows never got therunAs: 'system'treatment the scheduled ones did — every system-driven write refuses their data ops (12 failed runs on one boot) #684 approval run now pauses with a realsys_approval_request. Flows withrunAsdropped are refused atPOST /automation(422flow-update-readonly-field).lead_assignmentis split in two: a web-to-lead submission gets its SLA and is stored ownerless, and an integration-written owned lead (seed plus a user-less run) gets its SLA and one alert. The second half is the base fact.flow-run-summary: sweeps are read as deltas over a settled database.actedcounts the writes; notifications areunmeasured.flow-scheduled: case statusworking→in_progress; the breached case ishigh(acriticalcase is escalated on create). "Does not re-process" is now asserted on an unchangedupdated_at. The renewal window is pinned at both edges by behaviour instead of by the recorded query. forecast_snapshot 与启动重播种的互动:每次 dev 重启后,当季出现一条无 owner 的幻影快照行与 owner 键控行并存 #702 runs a real cold boot and a warm replay.flow-sla-ownerless-case/-assignment: each runs per datasource with the shapes a write can produce. Counts per run are 5 → 4.forecast-snapshot-amounts: a null amount is refused by the engine (required), so that case is pinned as the reason.knowledge-deflection: the blank article is now asserted as a null stored column.account-name-normalized-match: whitespace-only names are refused (they used to fold to null). An unwrappedLOWER({x})lands verbatim as text, so its comment is corrected. "A lead with no match key stops the conversion" is now pinned as a measured defect (F7).opportunity-*-approval-gate: "INPUT-FIRST" runs end to end through real approvals. A rep's write carrying the readonly qualification verdict is stripped and refused withRECORD_LOCKED(the expectation flipped).Hooks (
13d8d742)guestInsertnow uses the anonymous form door's execution context:publicFormGrant,guest_portal,anonymous(@objectstack/restregisterFormEndpoints). Before, it used an empty context that no door produces.guest-submission-sanitisation: the planted owner is stripped, and the case lands unowned, not with the agent (the grant cannot read the agent pool).flow-record-changeweb lead: stored ownerless.priority-rank-parity: "unknown priority falls back to 0" cannot be reached, becausepriorityis a required select that the engine refuses on every writer. It is pinned as the refusal on both objects.ownership-modelround-robin: ownerless intake is now a system seed. Measured: under an empty context the count reads 0 (first holder wins), and under the form context the pool read is denied.forecast-period-end-boundary: "gate can fail" boots the shipped artifact with the rule removed. The legacy row is a real upgrade: the app without the rule writes to adatabaseFile, then the shipped app cold-boots over it. "Refuses an UNRELATED edit to a row stored wrong": a{quota}-only edit is now admitted and re-derives the window (the hook runs on every update). The refusal is pinned on the form's payload, which carries the staleperiod_end.contact-email-tenant-scope: runs on an org-bound boot (orgContext: true) plus the untenanted one. "Reads org off the session" is re-aimed at the resolvedtenantId. "Skips the guard when org cannot be resolved" is measured on the untenanted boot: the index refuses (DUPLICATE_RECORD), not the hook.converted-lead-guard: valid values are used (email, rating ≤ 5). A label of company alone is unreachable because names are required.case-first-response: the event payload cannot carryfirst_response_date(unknown field), so that case is re-aimed. A denied read is now an agent who cannot see the case. "No api" is re-aimed at an imported held event.line-item-conventions: no product or no catalog price is refused (both are required). "No api" is re-aimed at a system-imported line.knowledge-feedback: readers vote through the actions (their own insert is refused bycontrolled_by_parent). An admin-withdrawn vote stays counted (F12, pinned).campaign-member-lifecycle: the CONTACT round trip runs as the admin. A marketing user's contact unsubscribe never reaches the contact (F13, pinned).demo-staffing: the table's people are signed up, the territory rules are re-evaluated throughPOST /sharing/rules/:id/evaluate, and accounts are read as each rep (6 / 2 / 1, as before). The escalation hand-off works on a system escalation. An agent's own escalation keeps the case (F14, pinned).case-assignment: pools are real position holders, released after each case. "Reads the pool literal" moved to the shipped body text, because reads are not observable. The web-to-case submission is never round-robined (F9, pinned). "Reverse verification: swap the order" is moot once the forward assignment cannot run for a guest; it is replaced by the stripped-owner pin.case-sla-matrix: unknown tier and unknown priority are refused by the select. The critical / no-api case is re-aimed at the anonymous submitter.freeze-guard-reference-cleanup: "a null over an already-empty link" is refused on the sparse datasource (same fact). On SQL it is no change and lands; this new case pins the divergence.hooks-runtime: the price fill keeps a negotiated price. Closed deals and accepted quotes are skipped for a system line. Clearing the last line leaves the amount at the last rollup. A person's suppliedstage_entry_datelands NULL (F10, not pinned; reported).hooks-runtime-service(107):low.working→in_progress.resolution_time_hoursis measured from a case opened 24 h earlier (the hook stamps the close).Ada Lovelace - Acme.Metadata / tenancy / actions (
e0a85770,69a26060,49b229a3)metadata-fixtures: the platform-object set now comes from the registry (72 roster names → 73 registry names: the registry adds the security plugin's RBAC objects and omits slate-only storage/job/email objects). No reference verdict changes.posture;OS_ALLOW_LAX_VALUE_SHAPESwhere warn-first is the subject.Static it() call sites across all test files: base 2462 → head 2375. The seven deleted self-proofs held 131. The 172 test files run 3563 tests.
Platform gaps: one local path each in
test/helpers/verify-stack.tsNone of these re-implements engine behaviour. Each is the engine's own service on the verify-booted kernel.
CAPABILITY_PROVIDERSis not exported, so the five pluginsrequires[]resolve to are named inextraPlugins.systemUpdate: there is no system-context UPDATE door.seedonly inserts, andhooks.runalways runs as a person.predicateUpdate: there is no door onto the engine'smulti: truepredicate update. RESTupdateManyiterates by id.guestInsert: the anonymous form doorPOST /forms/:slug/submitis not served by the handle's dispatcher (measuredENDPOINT_NOT_FOUND). The door's execution context is reproduced instead.runRecordFlow: there is no door for a user-less record trigger or for a record the engine no longer holds. Every handle write fires as a person, andseedfires no flows.recordEngineWrites: there is no way to observe what a hook handed the engine, when anasynchook finished, or a staged refusal.runShippedHook: the handle boots the source config, so there is no door onto the lowered-body path (the production refusal envelope).conditionHolds:automation.evaluateConditionis a kernel service. It is used for truth tables over row shapes that no write produces.sharingRules.evaluateRuleis called as a kernel service in two suites, but the REST doorPOST /sharing/rules/:id/evaluateworks on the handle (demo-staffinguses it). This is a cleanup follow-up, not a platform gap.Count: 8 local paths (1–8).
Helper fates
Deleted (5):
hook-harness,flow-harness,action-sandbox,metadata-fixtures,tenancy-probe.New (1):
verify-stack.ts, which holds the shared boot options and the eight gap paths.Kept, as the card expects (4):
repo-root,heading-label,docs-anchors,persona-vocabulary.Kept, deviation from "only the four" (6):
composed-stack(108 importers): the static composed stack. It gained the authored collections frommetadata-fixtures, and it only reads authored metadata.src-roster,config-globs,flow-regions,registration-lists: static source/metadata readers.identity-objects(4 importers): still used by suites that hand-build anObjectKerneland are not among the five stand-ins' importers (e.g. thefreeze-guard-reference-cleanupcascade block).None of these executes app behaviour.
Dependency diff
package.jsongains, at the exact pin:@objectstack/verify17.7.0,@objectstack/plugin-audit17.7.0,@objectstack/plugin-email17.7.0 (devDependencies). Every@objectstack/*a test imports is declared (21 packages;git grep -h "from '@objectstack/" testvspackage.json). The card's aim of "verify the only test-side entry besides spec" is not met: tests still importobjectql,runtime,cli/hook-body, the drivers and others directly, all declared.Measured defects the real engine surfaced (pinned with "⚠️ measured defect" where the base asserted the opposite; full evidence in the report)
get_recordprojection widened: platform columns sent to the billing endpointcrm_opportunity.crm_accountis required and does not cascade, so the engine refuses it (DELETE_RESTRICTED,crm_opportunity)DELETE_RESTRICTED,crm_contract)stage_entry_datelands it NULLid)Retitle round (
70084d9c): titles that kept the base wording while the asserted outcome flippedA test's title must say what its body asserts. These cases kept their base
99d290adtitle, but on the real engine the asserted outcome flipped (allowed → refused, written → refused, omitted → written). Each one is retitled. Defects use the⚠️ … (measured defect)form, with a comment and a "the defect is fixed: rewrite this case" message. The rest are reworded to what the body asserts. No matcher changed. The contact case is split in two so that each half has a true title, and the whitespace-only fold row moves to its ownit.eachtable.test/hooks-runtime-sales.test.ts:623test/hooks-runtime-sales.test.ts:679test/hooks-runtime-sales.test.ts:900test/hooks-runtime-sales.test.ts:920test/hooks-runtime-sales.test.ts:632test/hooks-runtime-sales.test.ts:211test/hooks-runtime-sales.test.ts:475test/hooks-runtime-sales.test.ts:884account_protection)test/account-name-normalized-match.test.ts:359lead_duplicate_check)test/account-name-normalized-match.test.ts:403test/escalation-task-subject.test.ts:127test/flow-campaign-enrollment.test.ts:163test/flow-campaign-enrollment.test.ts:220test/forecast-snapshot-amounts.test.ts:77test/opportunity-qualification-approval-gate.test.ts:293test/quote-accepted-lookups.test.ts:253test/record-id-not-in-prose.test.ts:109test/record-id-not-in-prose.test.ts:157test/record-id-not-in-prose.test.ts:223test/record-id-not-in-prose.test.ts:290How the sweep was run:
it/testcall was parsed with the TypeScript compiler API. Base: all 178 test files at99d290ad, including the deleted suites. Head: every test file.Checked and left as is (the title is still true; not a flip):
contact-email-tenant-scope.test.ts:160: the guard is still skipped. The unique index refuses the duplicate, and the test tells it apart byDUPLICATE_RECORDvs the guard'sDUPLICATE_VALUE.flow-billing-handoff.test.ts:336:active"does not exist" and is now refused at the save.runAsis dropped" cases: the flow is now refused atPOST /automationinstead of at run.flow-scheduled.test.ts:738"coerces a string amount": SQLite returns the string as a number. The comment says so, and a sum is still asserted.global-actions.test.ts:483/script-bodies.test.ts:437"reads sys_user once — and only because the dispatcher delivered no name": the delivered-name half is no longer asserted, because no door delivers a name on 17.7.0. A half was dropped, but nothing flipped. Reported, not retitled.Gate on the new head:
pnpm verifyran on70084d9c(git rev-parse --short HEADprinted by the run itself), under the shared verify lock →os-verify-lock: VERDICT command-exit 0(held 8m44s). Every stage reported the same as on632787ab: validate ✓, typecheck clean, lint 0 errors / 0 warnings / 18 suggestions, i18n gate ✓, hygiene ✓, tokens ✓, build ✓ (the same 8 author-time warnings),Test Files 172 passed (172) · Tests 3564 passed | 1 skipped (3565). The one extra test is the split contact case.--coveragewas not re-run, because no matcher changed.#2000 coupling
claude/issue-2000-picklist-metadata@c8f331cf(not touched) edits 7 files this PR also edits:global-actions,hook-org-inheritance,i18n-references,opportunity-line-item-add-picker,placeholder-picklist-options,quote-accepted-payment-terms,view-references. #2000's versions of all seven still import a deleted stand-in (metadata-fixtures/action-sandbox/hook-harness). Whichever lands second must rebase: if this one lands first, #2000 has to move its additions onto the verify handle. #2000 also changessrc/picklists that several ported suites read at runtime.Gates
Both gates ran on the final commit
632787ab(git rev-parse --short HEADprinted by the run itself), each under the shared verify lock.pnpm verify→os-verify-lock: VERDICT command-exit 0(held 8m13s):validate:✓ Validation passedtypecheck: clean (it covers the 173*.test.tsfiles, checked with--listFiles)lint(objectstack lint --strict): 0 errors, 0 warnings, 18 suggestionslint:i18n-gate:✓ i18n lint gate: 0 i18n/missing-* issueshygiene:✓ source hygiene cleanhygiene:tokens:✓ source token ratchet cleanbuild:✓ Build complete, "all 42 callables are body-only", with 8 author-time warnings, allapproval-approvers-may-resolve-emptyon approval flows this PR does not touchtest:Test Files 172 passed (172) · Tests 3563 passed | 1 skipped (3564)pnpm test --coverage→VERDICT command-exit 0, same counts. Coverage scope issrc/*/objects/*.hook.ts. The thresholds invitest.config.tsare unchanged (95 / 92 / 92 / 78) and met:99d290ad632787abAn earlier
pnpm verifyon330517ffwas red in exactly one file,runtime-coverage(2 cases): a ported hook and three flows were no longer named in a runtime file. Commit632787abfixes it, and both gates above ran after it.Acceptance notes
service_agentdoes not re-grant the triage rule on existing unowned cases until a reconcile.srccomments (⛔ not edited here; carrier [finding] 26 source comments in src/ cite src/flows/ — a directory ADR-0130 removed (blocked on Track A: file surface collides) #1919) still cite deleted suites:_hook-api.ts:55,account.object.ts:271,lead.hook.ts:199,case.hook.ts:75(hook-query-predicate/hook-input-shape);campaign.hook.ts:35,_line-item-price-fill.ts:31,activity-actions.ts:118,122,_hook-api.ts:158,_territory.ts:35,account.hook.ts:106,170,lead.campaign-metrics.hook.ts:24,opportunity.campaign-metrics.hook.ts:26(action-sandbox).objectstack.composition.ts:339-340too (shipped config file, left alone).billing-handoff-closed-won.flow.ts:70/contract-activated.flow.ts:70/opportunity-won-alert.flow.ts:70say that a bulkupdateManybindspreviousas null; 17.7.0 binds the per-row pre-image.AGENTS.mdchanged in two citations only (1f952f98). Maintainer to confirm.filter-key source scan in the deletedhook-query-predicateis covered by theHookQuerytype, which has nofiltermember (an excess property is a compile error underpnpm typecheck).632787abis a follow-up fix forruntime-coverage, which went red after the flow and hook ports because three flows and one hook were no longer spelled in a runtime file.Generated by Claude Code