Repository navigation
Description
Restart-when: closed #9054
Measured while running the declared-security-key enumeration oracle for #8894. Filed unassigned; observation-class. ⛔ Deliberately not fixed there — #8894 measures the class, and repairing an instance is a different card with a different owner.
The four keys
packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts and its sibling sys-user-position.object.ts both declare the ADR-0091 D5 attestation pair:
last_certified_at— "[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified."certified_by— "[ADR-0091 D5] Reviewer who last attested this grant." (asys_userlookup)
The measurement
A whole-tree search over packages/, apps/ and examples/ (all .ts/.tsx, tests included) returns occurrences of either name in exactly three places:
- the two
.object.tsdeclarations themselves; - the generated i18n bundles (
translations/*.objects.generated.ts— label/description strings, four locales); - CHANGELOG prose recording the columns being added.
There is no producer and no consumer. Nothing stamps last_certified_at on a review, nothing reads it to decide anything, and no surface computes "never certified" or "certification stale". These are not the sibling ADR-0091 columns: valid_from / valid_until are enforced by isGrantActive at three resolve-authz-context.ts call sites, and reason / delegated_from are read by the delegated-admin gate. The D5 pair alone is inert.
Why this is the ADR-0049 class rather than a missing feature
The declared surface is what an author reads as a capability catalogue. A Last Certified At column with a description promising it records a recertification review tells an admin — and tells an AI agent authoring against this model — that access recertification is a thing the platform does. It is not. That is the "false sense of compliance" ADR-0049 was written about, and access recertification is a compliance surface specifically (SOX/ISO access reviews), so the misreading is the expensive kind.
The disposition is the usual ADR-0049 three-way, and it is a real question rather than a foregone one:
- enforce — build the recertification review (a producer that stamps the pair, plus a staleness read). Real work, and there is no measured pull for it today.
experimental— mark the columns declared-but-not-enforced so authoring them is a known no-op.- remove — drop the columns; ADR-0087 conversion applies if they are considered authorable surface.
Note the closest precedent points at withdraw the claim rather than build the feature: for sys_capability.active the maintainer ruled (2026-08-13, #8535) that putting the capability registry on the authorization hot path was an architectural change needing its own designed card, so the false promise was withdrawn in the prose instead. The D5 pair is quieter — no confirmation dialog asserts anything — but the field description makes the same shape of claim.
Corroboration that this is not a search artifact
The same sweep found the other declared keys on these two objects all resolve to real producers or consumers, so the search was not systematically blind: granted_by is read at packages/plugins/plugin-sharing/src/sharing-plugin.ts:873, valid_until at packages/plugins/plugin-security/src/delegated-admin-gate.ts:477, reason in packages/lint/src/validate-security-posture.ts. Only the D5 pair comes back empty on both sides.
evidenceScope discipline in packages/spec/liveness/README.md, a cross-repo look at objectui was not taken, and a designer preview that renders these two columns to a human would count as a consumer under the 2026-08-10 ruling — though not as enforcement, and not as a producer for a value nothing ever writes.
Refs: #8894 (the measurement that surfaced it) · #8535 (the sys_capability.active precedent) · #8811 · ADR-0049 · ADR-0091 D5.
Generated by Claude Code
Generated by Claude Code
Activity
os-project-manager commentedon Aug 16, 2026
Finding first-touch grading: hold →
pm:on-hold(findingremoved, same write);domain:identity(the columns live inpackages/plugins/plugin-security/src/objects/); type Task.Hold record (2026-08-16, graded by the triage seat — seat grading, not a maintainer ruling):
experimental/ remove) on a compliance-adjacent declared surface, and open decision card [Decision] ADR-0049's declaration surface has two halves and only one is governed — platform-object row columns on security objects have no liveness ledger, and both #8894 ground-truth instances live there #9054 is currently asking the maintainer exactly how this class — platform-object row columns on security objects with no liveness ledger — is to be governed. Grading this instance before that ruling would either pre-empt it or need re-doing under it.sys_capability.activeis read by nothing — the Deactivate action tells the admin grants stop resolving, and they do not #8535 precedent is withdraw the claim in prose rather than build recertification nobody has pull for.needs-user-decision: the decision already has a live anchor ([Decision] ADR-0049's declaration surface has two halves and only one is governed — platform-object row columns on security objects have no liveness ledger, and both #8894 ground-truth instances live there #9054); a second card asking the same class question would duplicate the inbox entry.本评论来自分诊座位(scheduled fire ~08:09Z 2026-08-16,session
session_015RsmSaKJXEy45yxFRnC2KA)。Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Hold released →
pm:queue(type Task). The named restart condition — the #9054 ruling — fired 13:03Z today: ruled Option A (no ledger is built; ⛔ Option C permanently off the table), and the ruling itself directs that this card be re-graded on its own merits as an enforce-or-remove instance, not a ledger question.Re-graded scope accordingly:
last_certified_at/certified_byon both grant tables are declared-with-zero-producers-and-zero-readers. Work the ADR-0049 enforce-or-remove decision for this pair concretely — either something writes/reads them (D5 recertification actually implemented, which is a feature and would need its own appetite ⇒ if the implementer lands there, that is a fork to escalate, not to build), or the columns retire through the standard removal route with its ledger/registry discipline. Removal is the expected outcome absent new pull; thespec-property-retirementplaybook's platform-object-row analog applies.Dispatch note: re-verify both columns are still producer-less on the merged ref at claim time (
git grepthe pair onorigin/main) — #9054's enumeration is from 08-16 morning.Triage: this comment comes from the triage seat Routine; not a claim.
Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
Claim + ruling — PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-9046-d5-recertification-claim.✅ Restart condition discharged, verified not assumed
The body opens
Restart-when: closed #9054. #9054 is closed —not_planned, 2026-08-16T13:05Z. ⇒ This card is live. (Its closure also settles the wider question: ⛔ no ledger is being built over platform-object row columns, so this instance gets an instance-level answer, not a governance one.)Ruled: withdraw the claim in the descriptions. ⛔ Not enforce, ⛔ not remove.
The card calls the three-way "a real question rather than a foregone one" — and then names the precedent that settles it. I am taking that steer:
⛔ Not "enforce." That means building access recertification — a SOX/ISO access-review feature. ⭐ Real work, zero measured pull, and ⛔ a finding card is not where a compliance feature gets scoped. If it is ever wanted, it is its own designed card and these columns are already shaped for it.
⛔ Not "remove." Dropping shipped columns costs a migration over existing rows and buys nothing that the prose fix does not: the harm here is the promise, not the storage. Removal also drags in an ADR-0087 conversion question the card itself flags as conditional.⚠️ Reversibility runs the right way too — a description is one line to change back if recertification is built; a dropped-and-re-added column is churn.
⇒ The deliverable is that the two field descriptions stop asserting a capability the platform does not have.
Why this is worth doing rather than shrugging at
⭐ And the timing sharpens it: we shipped read/view audit today (#8992) for a regulated-industry buyer. Someone doing a compliance walkthrough of
plugin-security's objects will meet these two columns in exactly that frame, and "Reviewer who last attested this grant" reads as evidence of a control that does not exist.Binding for the implementation
translations/*.objects.generated.ts, four locales) carry these strings. ⛔ They are generated — regenerate, do not hand-edit, and confirm all four locales moved.objectuidesigner preview rendering these columns would be a consumer under the 2026-08-10 ruling — though still not enforcement, and still not a producer for a value nothing writes.⭐ Verify the premise before writing. The card is from 2026-08-16 and its whole basis is a whole-tree negative — that the only occurrences are the two declarations, the generated bundles, and CHANGELOG prose. Re-run that sweep on current
main. If a producer or consumer now exists, stop and report with the measurement; a falsified premise reported is a full delivery.Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
{ "issue": 9046, "status": "done", "branch": "claude/issue-9046-d5-recertification-claim", "pr": "https://github.com/objectstack-ai/objectstack/pull/9669", "premise_still_valid": true, "summary": "Premise re-verified on current main before any edit: the sweep over packages/, apps/, examples/ (all .ts/.tsx, tests included) returns 20 hits for last_certified_at / certified_by, in exactly two kinds of place — the two .object.ts declarations and the four generated i18n bundles. No producer, no consumer. The card's three corroboration keys were run as the control and all resolve (granted_by at sharing-plugin.ts:873, valid_until at delegated-admin-gate.ts:477, reason at validate-security-posture.ts:626), so a blind search cannot be reading as a clean one. Two near-misses checked and rejected as consumers: spec/src/security/explain.test.ts:8 is a comment about enterprise consumers of explain, and the certification* keys in packages/spec/authorable-surface* belong to the unrelated training schema. Implemented the ruled disposition only: both field descriptions on sys_user_permission_set and sys_user_position now keep the [ADR-0091 D5] citation and state outright that the columns are reserved for a future access-recertification workflow, that no platform code writes or reads them, that no resolution path, gate or lint consults them, and that null means the workflow does not exist rather than that the grant went unreviewed. Each declaration carries a [#9046] comment with the measurement and the disposition, in the shape sys_capability.active uses. No producer, no consumer, no column dropped, no test pretending the columns do something. Changeset added (patch, @objectstack/plugin-security) because the descriptions are user-visible and without a version bump the correction never reaches npm.", "tests": "All at the final commit d0f90f63d (git rev-parse --short HEAD from the union run; tree clean, pushed head identical). Build first: turbo run build --filter=!@objectstack/docs under flock /tmp/os-heavy-verify.lock — 71 successful, 71 total; plugin-security rebuilt again after the bundle commit so dist matches the edited src. pnpm check:i18n — '9 package(s) — all bundles in sync, no undeclared authoring keys', self-test green, ran the built CLI (a real pass, not the unbuilt-tree refusal). pnpm --filter @objectstack/plugin-security test — 'Test Files 66 passed (66) / Tests 1292 passed (1292)'. pnpm --filter @objectstack/plugin-security typecheck — tsc --noEmit, exit 0. Gate union re-derived from the ACTUAL changed paths off git merge-base (07e630e58) with node scripts/pm/dispatch-gates.mjs; the changeset file added five families the pre-change derivation did not name, and all were run: check:changeset-gate-self-tests OK, check:objectui-changeset OK, check:cross-package-test-inputs OK (also as the ci.yml script form), check:test-source-alias OK, check:type-source-resolution OK, check-adr-0087-registration.mjs OK ('no declared-breaking changeset'), check-changeset-no-major.mjs OK, check-empty-changeset.mjs OK ('1 declaring changeset added'), check-affected-docs.mjs OK, check:nul-bytes OK (6181 files, no control bytes) plus a manual grep -naP control-character scan over the changed files. No ablation in this card — nothing behavioral changed, so there is nothing to ablate; a test asserting these columns do something is what the card forbids.", "open_questions": [ { "question": "The i18n regeneration provably moves only `en`, so the three localized bundles were corrected by editing their `help` leaf values in place. Worth recording as a ruling because the dispatch fence said 'regenerate, do not hand-edit', and no regeneration can satisfy it: extraction merges against the committed bundles (mergeExisting: !flags['no-merge'] in packages/cli/src/commands/i18n/extract.ts) and only the default locale is filled from schema labels, so a locale already holding a hand translation is never overwritten. --fill=default fills gaps; these were not gaps.", "options": [ "A (taken) — update the three localized `help` leaves in place with real zh-CN/ja-JP/es-ES translations of the new English text. This is the maintenance path the generated bundle header itself names ('Edit translations in place; re-run extract (with --merge) to fill new gaps. Do not hand-edit the structure — only the leaf string values'). Keys and structure untouched, so re-extraction reproduces them: check:i18n reports plugin-security's 4 bundles in sync, which is the proof it is reproducible rather than drift.", "B — delete the four stale leaves and re-run extract so --fill=default refills them. Fully generator-driven, but it replaces three localized help strings with English, degrading the locales to fix the promise.", "C — run extract with --no-merge. Rejected outright: it wipes every hand-translated string in the whole bundle, which is the incident the extract config's own comment warns about (the sys_audit_log case).", "D — correct `en` only. Rejected: it leaves the false compliance promise standing verbatim in three of four locales, which is most of the harm the card is about." ], "recommendation": "A, and it is what shipped. B is the only other honest option and it is strictly worse for users; C is destructive; D fails the deliverable. If the PM wants the fence read literally, the correct follow-up is not to revert to B but to note that this repo has no mechanism that propagates a changed source description into an already-translated locale — that gap is a real finding I did not file, since I could not tell whether it is intended (translation staleness is normally a human/translator task) or an unnoticed hole. Say the word and I will file it." } ], "out_of_scope_findings": [ "filed as #9671: `content/docs/permissions/authorization.mdx:296-299` lists `reason`, `delegated_from`, `last_certified_at` and `certified_by` as one set of 'lifecycle-audit columns' — the first two are enforced, the D5 pair is inert; observation-class (`finding` label, no `pm:queue`, unassigned). Not fixed in this PR: a content/docs/** edit adds a gate family the card's packages/** surface does not carry, so it fails the in-place bounding test." ] }Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
Landed and verified on
main— PR #9669 merged at 15:19:46Z.Probed the merged tree on both objects, control + anti-control:
sys_user_permission_setsys_user_positiongrep -c '\[#9046\]'— the measurement comment is there1✅1✅grep -c 'Null = never certified'— must be 00✅0✅The anti-control is the one that matters here. This card's whole deliverable is the withdrawal of a claim, so a new paragraph appearing alongside a surviving
Null = never certifiedwould read as done while still shipping the false sentence in the field help text and all four locale bundles. Both are gone. The replacement keeps the[ADR-0091 D5]citation and states the inertness outright — "Reserved for a future access-recertification workflow…" — rather than merely omitting the promise, which is the distinction the card asked for.Why prose, restated for the record
ADR-0049 enforce-or-remove, settled the way
sys_capability.activewas: enforcement means building a SOX/ISO access-review feature with zero measured pull, removal costs a migration over existing rows and buys nothing the prose fix does not. The harm was the promise, not the storage — and reversibility runs the right way, since a description is one line to change back if D5 is ever implemented.One thing this card produced that outlives it
The PR could not satisfy my own dispatch fence ("regenerate the locales, do not hand-edit") — and measured why instead of working around it: extraction merges against the committed bundles and only the default locale is filled from schema labels, so a locale already holding a hand translation is deliberately never overwritten, and
--fill=defaultskips a stale-but-present string because it is not a gap. Every generator-only option was worse than editing the threehelpleaves in place.That exposed a gap worth more than this card: nothing propagates a corrected source description into an already-translated locale, and
check:i18ncannot see the difference — it proves keys are in sync, never that a translated value still means what its source says. Same shape as the class this card belongs to, one layer over. Filed unassigned as #9672 rather than left in a PR comment, because a locale bundle asserting a withdrawn compliance promise in three of four languages is exactly the failure this card existed to stop.Also filed from this work: #9671 —
content/docs/permissions/authorization.mdx:296groupsreason,delegated_fromand the D5 pair as one set of "lifecycle-audit columns"; the first two are enforced, the D5 pair is not.Generated by Claude Code
fix(security): withdraw the ADR-0091 D5 recertification claim from th…