fix(spec): grade flow.description and hook.label/description live — Studio's metadata list and quick-find show them - #20541
Conversation
…he Studio metadata list and quick-find show them Three docs-shaped liveness rows move dead -> live. Neither `flow` nor `hook` registers list columns in the Studio metadata admin, so the list page's default columns draw `label` and `description`, and the metadata quick-find draws both as well. Each row cites that reader at the `.objectui-sha` pin, names the producer (route, metadata client read and the shared `createMetaListAnswer` list answer), and keeps its old note as history. A booted showcase read of GET /api/v1/meta/flow and /meta/hook served every authored label and description. state-counts.md regenerated; the README flow/hook Notes cells and the liveness gate test that borrowed flow.description as its sample dead row move with the flip. No schema, parse or describe change. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: PR #20541 (draft; head repo is the base repo; branch ① Derived judgmentsAccept-set and public surface. The diff touches no schema, no The lane's call is inside the ruling, from the README's own words. The decisive sentence of the #7131 section is not the preview enumeration but the premise of its table: "The two rules divide on what the property claims, not on what the surface is", with the display row: "Being shown to a human is the whole of the claimed effect. There is no second layer where the 'real' consumer would live, so the preview is not standing in for anything." The mechanical preview lookup is the MINIMUM a sweep takes before writing "no runtime consumer" (an absence to record, not a whitelist of surfaces), and the section's closing move condemns exactly "taking a preview's absence from the search as proof that nothing reads the value, for a key whose only job was ever to be read by a person". A Studio metadata list column and the metadata quick-find are metadata-admin surfaces that show the key to a human, so for Reader truth at the pin
Producer truth in this repo, read at Row shape, against the README and the Out-of-claim files: both required, both minimal.
Gate coverage, from the one read of the check-runs on ② Semver level
Clause-②: no. No accept-set change, no widening, no narrowing; the PR body's line matches. ③ Boundary flagsDev flags from
Out-of-scope findings: (a) Nits, no action owed on this head: the rows' "no projection except the translation step" could say the step is a no-op for these types; the Studio list's project-package scope is a display condition worth one clause in a future sweep. Owed later, not here: the merge-and-regenerate round against PR #20532 if it lands first. Implemented-by: VERDICT: PASS Generated by Claude Code |
Brings in the sharded liveness counts. The modify/delete on packages/spec/liveness/state-counts.md is resolved by keeping main's deletion; the per-type shards under state-counts/ are regenerated in the next commit. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
… the merge gen:liveness-counts over the merged tree rewrote exactly two shards, state-counts/flow.md (35 live / 5 dead) and state-counts/hook.md (21 live / 1 dead): the three flips of this branch. No other shard moved. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
… of the retired state-counts.md The counts moved into per-type shards on main; the changeset's last bullet still named the deleted file. Only that bullet changes. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Part of #20299
Clause-②: no
Stage 1 of #20299. Three docs-shaped liveness rows move
dead→live, because Studio already shows them to a human:flow.description,hook.labelandhook.description. The other four rows of the family need objectui code and are objectstack-ai/objectui#11027's:app.areas.description,permission.rowLevelSecurity.label/.descriptionand theviewcontainerlabel. They are untouched here, and #20299 remains open for them.Ledger data, two README Notes cells and one gate-test fixture only. ⛔ No schema, parse,
.describe()or accept-set change. None of the three rows setsauthorWarn, so the set of lint warnings does not change. The re-grade reverses no ADR-0033 decision: all three stay docs-shaped, deliberately KEPT and exempt from enforce-or-remove. Each row keeps the note it carried whiledead, labelled as history.Files
packages/spec/liveness/flow.json: thedescriptionrow.packages/spec/liveness/hook.json: thelabelanddescriptionrows, plus one dated sentence appended to the file_note. Its 2026-08-10 lookup ("both verdicts stand unchanged") is now history.packages/spec/liveness/state-counts/flow.mdandpackages/spec/liveness/state-counts/hook.md: the two per-type count shards, regenerated bygen:liveness-counts, not hand-edited.flowhas 35 live and 5 dead (was 34 / 6).hookhas 21 live and 1 dead (was 19 / 3). Across all shards, the read-time sum thatcheck:livenessprints is 952 live and 136 dead (was 949 / 139). No file commits that total.packages/spec/liveness/README.md: theflowandhookNotes cells. Both listed these keys as dead.packages/spec/scripts/liveness/check-liveness.test.ts: the "stays GREEN when adeadentry carries the SAME rotted pointer" case borrowedflow.descriptionas its sampledeadrow. It now uses theflow.activetombstone, which the gate itself holds atdead, and it asserts that precondition..changeset/20299-display-annotations-ledger.md: apatchchangeset for@objectstack/spec. The ledgers ship in itsfiles[](liveness), and@objectstack/lintreads them.File-surface declaration. The claim's surface named
flow.json,hook.json, the regenerated counts and the changeset. The README cells and the test fixture are outside it. The dispatch's pin sweep required both to move with the flip ("grep for any test, doc or ledger note that asserts these three rows aredead… and move it with the flip"). Without the fixture move, the gate test goes red: see the reverse verification below. The README ships in the samelivenessdirectory.The lane's call, and the ruling it applies
A Studio list column and the metadata quick-find count as consumers of a DISPLAY-shaped key. That is the #7131 ruling's table in
packages/spec/liveness/README.md("Designer previews count as consumers"): for a display key, being shown to a human is the whole of the claimed effect. The README'sproducerdiscipline binds too, so each row names who hands the reader a record.Premise, reader half: measured at the
.objectui-shapindd3f7e1beInstrument: read-only
git -C ../objectui show SHA:PATHandgit grep … SHA. Nothing in objectui was edited, checked out or stashed.metadata/:typemountsMetadataResourceListPage(packages/app-shell/src/console/AppContent.tsx, both the with-app and the zero-app branch).MetadataResourceListPagerenders a registered customListPageif one exists, and otherwiseDefaultMetadataList.DefaultMetadataListtakesconfig.listColumns ?? defaultColumns(config.primaryKey ?? 'name').defaultColumnsreturns the primary key,labelanddescription, and each cell goes throughdefaultCell.floworhookaListPageorlistColumns.git grep registerMetadataResourceoverpackagesandappsgives 44 hits. The non-test registrations arebuiltinComponents.tsx(object, field, permission, view, dashboard, page, book),anchors.ts#registerBuiltinAnchors,datasource/register.tsanddefault-schemas.ts, which setsdefaultSchema/fieldOrderonly.flowandhookregister only inanchors.ts, with anchors, create fields and defaults.git grep -E 'ListPage\s*:'hits onlydatasource/register.ts, the positive control. The shorthand spellingListPage[,}]/listColumns[,}]has no registration hit.MetadataQuickFind(QuickFind.tsx) indexes every type's items off the sameclient.list(type)read. It keepslabelanddescriptionand drawslabelbeside the name anddescriptionunder it. It is mounted onDirectoryPageandStudioHomePage.MetadataClient.list(type)(packages/data-objectstack/src/metadata-client.ts) isGET {base}/{type}, with base/api/v1/meta. It accepts a top-level array oritems.5d689c3, counted withgit show REF:PATH | grep -cFat both refs. Every cited string counts the same at the pin and at main (1/1 each): thelistColumns ?? defaultColumnsline, thedescriptionandlabeldefault-column entries,if (customConfig?.ListPage) {, the:typeroute element, QuickFind'slabel: item?.label,,description: item?.description,and{r.description}. TheListPage:andlistColumnsregistration counts also match.Premise, producer half: measured booted, not read
Instrument: a throwaway
@objectstack/verifytest inpackages/qa/dogfood(deleted after the run, never committed). It booted the realexamples/app-showcasecomposition in-process withbootStack(showcaseStack), signed in as the dev admin and read the exact doors the list page reads. It was run twice, before and after a container restart, with the same result.GET /api/v1/meta/flowtype,itemslabelanddescription, with_packageId: com.example.showcaseGET /api/v1/meta/hooktype,itemslabelanddescription, with_packageId: com.example.showcaseGET /api/v1/meta/packagetype,itemscom.example.showcasewithscope: project, which is whatbuildPackageScopeOptionsadmits, so the list page's package scope shows those rowsThe server-side list answer is
packages/rest/src/meta-item-read-gate.ts#createMetaListAnswer, which both transports serve. Its type-specific steps are forapi,app,view,docandobjectonly. Forflowandhookit runs the per-caller gate and the translation step, and neither dropslabelordescription. Each row'sproducercites that answer, the route, the list-page fall-through, the registration and the client read.Verification, at
cb0206219cAll heavy runs went through
scripts/pm/os-verify-lock.shwith--maxWorkers=2. The box is shared, so wall-clock figures are shared-box readings.pnpm --filter @objectstack/spec run check:liveness: exit 0. "✓ every governed-type property … is classified …" and "✓ packages/spec/liveness/state-counts.md is current".pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2(the wholelocalproject): 573 files, 16835 passed, 1 todo.@objectstack/specrepoproject, narrowed. The wholerepoproject hit its 330 s timeout on the shared box, so that run is NOT MEASURED. It was narrowed to the 10repofiles that read the ledgers:scripts/liveness/evidence.test.ts,scripts/liveness/proof-registry.test.tsand eight*-retirement/*.pintests. Result: 10 files, 211 passed. CI runs the whole project.pnpm --filter @objectstack/spec typecheck: exit 0. It coverstsc --noEmit,check:scripts-typecheckandcheck:test-typecheck.tsc -p tsconfig.scripts.json --listFilesnamesscripts/liveness/check-liveness.test.ts, so the edited test is compiled.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 68 commands atcb0206219c. All 68 ran, and each exit code was captured before any pipe.--ranover the exit-coded record reported: "✓ 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero …)".check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: eight unbuilt packages). It was re-measured after building them (all turbo cache hits) and exited 0: "104 published require entry point(s) across 66 package(s) load".check:platform-checklistexits 1 on a finding this diff does not reach. See Acceptance notes.scripts/ablation-replace.mjs(anchorsetEvidence(root, 'flow', 'active',hit ×1 → ×0, replacement ×0 → ×1, blob9ff17ad4→1091717a). The case went red:expected 1 to be +0, and the gate namedflow/description → packages/plugins/driver-sql/src/sql-driver.ts. That is the old fixture failing because the row is now scanned aslive. The tool restored the file: blob after restore9ff17ad4equalsHEAD, andgit diff HEADis empty. The direction observed was a turn to red, as expected.origin/mainhas moved to6427e2cf56since the basefb386074f5. None of the six paths changed upstream (git diff --name-only BASE origin/main -- PATHSis empty), so the regenerated counts need no merge.Acceptance notes
check:platform-checklistis red on the base and does not involve this diff.docs/qa/platform-checklist/areas/identity-auth.jsonanchorspackages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor. Since7d63088958(PR fix(plugin-auth): a refused auth setting no longer drops the settings saved with it #20429), that symbol exists in the file only as a member inside apatchobject, whichsymbol-anchors.mjsdoes not accept as a declaration. The files that finding names (the checklist area,auth-plugin.ts,scripts/check-platform-checklist.mjs,scripts/symbol-anchors.mjs,scripts/checklist-select.mjs) are byte-identical between this branch's basefb386074f5andorigin/main1378ec7c0c, and none of the six paths here is among them. Carrier: the plugin-auth / checklist owner. Not filed here.docs/audits/2026-06-flowschema-property-liveness.mdrecords the dated 2026-06 audit and is not a current-state claim..claude/skills/spec-property-retirement/SKILL.md§0 useshook.label/flow.descriptionas the example of "build the renderer, do not retire", and that is what happened.liveness/app.json(areas.description) andliveness/job.jsoncite "the hook.label precedent" for "docs-shaped, kept, not warned", and that is still true.QuickFind.tsx's docblock calls it a "Cmd+K palette", but it binds Cmd+Shift+M, to leave Cmd+K toCommandPalette. The rows say "metadata quick-find" and name no key.hookstill has no registered metadata-admin preview. AHookPreviewis not needed for these rows, and objectui#11027 already excludes it.Generated by Claude Code