feat(spec,lint): gate managed apiMethods ⊆ affordances where the author is (#7521) - #7851
Conversation
…thor is (#7521) `reconcileManagedApiMethods` has always caught a managed object advertising a write verb its own affordances refuse, and stripped it — fail-closed, nothing ever exposed. What it could not do is tell anyone: the only signal was a `console.warn`. `sys_environment` / `sys_package` declared `apiMethods: ['get','list','create','update']` against `userActions` refusing all three writes, and that warning fired on every control-plane boot for the life of the divergence unread. The split was found by hand-driving the HTTP seam, not by any gate. Per the maintainer's 2026-08-11 ruling (the middle option — lint/gate-visible): - **New shared predicate** `checkManagedApiMethodAffordances` (`@objectstack/spec/data`), beside `resolveCrudAffordances` — the affordance authority both sides already read. The verb → affordance table moves here from objectql's registry. - **`reconcileManagedApiMethods` is now a pure reaction to it.** Behaviour is unchanged and pinned so: still warn-and-strip, still never throws — failing registration closed would let one metadata typo kill a control-plane boot. The warning now cites the lint rule id so a boot log leads to the gate. - **New author-time rule** `object/managed-api-method-unaffordable` (`error`, gating, pre-parse) wired into the authoring registry, so `os lint`, `os validate` and `os build` all report it. One predicate, two consumers: a second copy of this table at either end would BE the declared≠enforced drift the rule exists to detect. Same shape, and same reason, as `checkFieldCompleteness` under ADR-0078. `validateManagedApiMethods` and its rule id are exported so a repo whose object definitions live in code — which `os lint` never walks — can run the same rule over its own registry instead of hand-rolling the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Rebase round for PR #7851 (#7521) after the domain:spec os-regen landing relay tail landed on main (#7758, #7759, #7763, #7782, #7813, #7892). Every `merge=os-regen` generated path is taken from origin/main WHOLESALE (`git checkout origin/main -- <paths>`, the 12 entries .gitattributes names), so this commit is a known-good base: the `os-regen` merge driver exits 0 without conflict markers while silently dropping one side, so a clean merge is indistinguishable from a dropped baseline. Regeneration lands as a SEPARATE commit on top of this one, and asserts the relay PRs' entries survived. Committed with --no-verify deliberately: the os-regen pre-commit hook wants the artifacts regenerated into this same commit, which would mix "what main brought" with "what my change produces" into one indistinguishable blob. The very next commit regenerates them, so the pushed tip is current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96
…ee (#7521) Step 4 of the os-regen rebase, on top of the committed merge (cbea40d) whose generated baselines came from origin/main wholesale. `check:generated` proved exactly 2 of 13 artifacts stale — api-surface/ and export-origins/, both this PR's own — and the other 11 current, so main's baselines carried across the merge intact. Regenerated only those two. The diff is 4 pure insertions and zero removals: the four exports managed-api-affordance.ts adds. The relay tail's entries are asserted present in the regenerated tree rather than assumed — the os-regen driver drops a side silently, so a clean merge is not evidence: #7763 TranslationBundle `flows` — content/docs/references/system/translation.mdx:161 #7813 Field.number `useGrouping` — packages/spec/authorable-surface/data.json #7892 GlobalFilterSchema `object` — content/docs/references/ui/dashboard.mdx #7782 InvitationStatus `canceled` — content/docs/references/identity/organization.mdx Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96
…ectstack-ai#8205) * fix(devx): teach the os-regen pre-commit hook the deferred merge (objectstack-ai#8047) Two in-repo authorities contradicted each other on main. `scripts/pm/os-regen-merge.sh` — the sanctioned landing sequence for a branch touching `merge=os-regen` paths — commits the merge BEFORE regenerating, deliberately: the driver exits 0 while silently dropping one side, so only a separate regeneration commit on a known-good base lets a reviewer read "what main brought" apart from "what the change produces". The `os-regen` pre-commit hook refused exactly that commit. The way out people learned (measured on PR objectstack-ai#7851) was to skip the entire pre-commit hook, which trades one false positive for a blanket bypass. Maintainer ruling 2026-08-12: the hook moves. The merge commit is now DEFERRED, not passed: `pre-commit` records `deferred-at <head> <merge-head>` in the existing marker and lets the commit through. Two properties keep that a split rather than an escape hatch. One commit deep, by construction. A deferral is entered only while MERGE_HEAD exists, a second merge attempted while one is outstanding is refused, and every non-merge commit after it is refused by the ordinary staleness check — so nothing can land between the merge and its discharge. "The immediately following commit" is enforced by there being no other commit it could be. Collected where collection becomes possible. At the moment the merge commit is created, the commit that discharges it does not exist, so pre-commit can only record. The two events that can follow are the next commit (this same check) and the push, so `.githooks/pre-push` — new, registered by the existing `core.hooksPath=.githooks`, 77ms and a marker stat on every push that did not defer — refuses a push that still owes one. An undischarged deferral cannot leave the machine. Prose reconciled across all three authorities that disagreed: the script header (the authority), AGENTS.md §11, and what the hook prints. The hook's refusal now names `scripts/pm/os-regen-merge.sh` instead of advertising a bypass. Tests: `--self-test` grows a decision table over all five states plus a fixture that builds throwaway git repos and replays the two-commit sequence — behaviour about a PAIR of commits, which no single-tree assertion can express. Gates are redirected at the fixture's own package.json so the replay costs no spec build; a mistake there fails safe (a directory without those scripts reads as stale). The pre-push hook's index mode is asserted too — git ignores a non-executable hook in silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q983j8EGKjUs8r14n76TeH * fix(devx): drop the issue-ID citation from the AGENTS.md deferral bullet `check:pm-skill-id-lint` refuses issue numbers in AGENTS.md operative text: a lesson has to be self-contained (failure mode + discipline + boundary) and a ruling keeps its date, so the number carries no information a reader needs. The bullet already states the mechanism in full; the citation becomes the ruling's date. Gate green locally, self-test included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q983j8EGKjUs8r14n76TeH --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #7521
Executes the maintainer's 2026-08-11 08:08Z ruling — the middle option, lint/gate-visible. Boot-time behaviour is unchanged and pinned so.
The window
reconcileManagedApiMethodshas always caught a managed object advertising a generic write verb its own resolved affordances refuse, and stripped it. That strip is correct and fail-closed — nothing was ever exposed. What it could not do is tell anyone: the only signal was aconsole.warn.sys_environmentandsys_packagedeclaredapiMethods: ['get','list','create','update']againstuserActionsrefusing all three writes. The strip and its warning fired on every control-plane boot for the life of the divergence and nobody noticed. The split was eventually found by hand-driving the HTTP seam while writing something else, not by any gate. A boot log is not an authoring surface.What landed
checkManagedApiMethodAffordancesin@objectstack/spec/data, besideresolveCrudAffordances— the affordance authority both sides already read. The verb → affordance table (MANAGED_WRITE_VERB_AFFORDANCE) moves here from objectql's registry; it is not copied.reconcileManagedApiMethodsbecomes a pure reaction to it. Behaviour unchanged — all 91 pre-existing registry tests pass untouched — and now explicitly pinned: it still warns and strips, and still does not throw. The warning additionally cites the new lint rule id, so an operator who greps a stripped verb out of a boot log lands on the gate.object/managed-api-method-unaffordable(error,gating, pre-parse) wired intoauthoring-rules.ts, soos lint,os validateandos buildall report it. Verified end-to-end through the built authoring registry against the exactsys_environmentdeclaration.Why the predicate is in
spec, not inregistry.tsThe dispatch suggested
packages/objectql/src/registry.tsas the predicate's home, with a pre-authorized crossing intoscripts/orpackages/lint. Measurement overturned that route, and the ⛔ non-negotiable — one predicate, never a second affordance table — is what forced the move:@objectstack/lintdepends on@objectstack/specand, by its own stated package contract, never on a runtime. It cannot import objectql's table.scripts/check-*.mjsgate cannot either: every gate script in this repo is a static source scanner, and a.mjscannot consume a TS predicate.@objectstack/spec/datais the only location both consumers can read.This is exactly the
checkFieldCompletenessprecedent (@objectstack/spec/kernel), which serves the registry's functional-completeness warning and@objectstack/lint'svalidate-functional-completenessgate from one predicate under ADR-0078 — the same problem, one ADR earlier, solved the same way.Why not a
scripts/sweep over this repo's object definitionssys_environment/sys_packagelive in the cloud repo, not here — ascripts/gate in objectstack could never have seen them. The issue's own framing is that "every repo authoring managed objects re-pays the silent-divergence cost", so the deliverable has to be shipped, reusable code.validateManagedApiMethodsandMANAGED_API_METHOD_UNAFFORDABLEare exported from the package root so a repo whose object definitions live in code (whichos lintnever walks) can run the same rule over its own registry instead of hand-rolling the table — which is what cloud#1235 had to do.Declared cross-package crossings
packages/specandpackages/lint, beyond the card'spackages/objectqlsurface.packages/lintwas pre-authorized;packages/specwas not — flagging it explicitly for the cross-seat declaration, with the measurement above as the reason.packages/objectql/src/engine.tswas not touched.Gates
Green locally: both mandatory ratchets (
check:query-options-erasure,check:type-check-debt— no baseline raised, no ledger entry raised), fullobjectql(185 files / 3278 tests), fullspec(379 / 9983), fulllint(71 / 1929), typechecks for all three packages, ESLint on every changed file, the fullturbo buildclosure (70/70, includingos buildover the example stacks with the new gating rule active), the spec surface baselines (api-surface/export-origins/dual-source-exports/exported-any/spec-changes/generated, regenerated and re-verified against a real.d.tsbuild), the changeset gates, and every gatescripts/pm/dispatch-gates.mjsderived for the changed paths.Changeset included.
content/docs/releases/untouched.Generated by Claude Code