Skip to content

feat(plugin-security)!: the six built-in positions are declared position metadata (ADR-0131 C2 stage S2) - #22139

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-15196-s2-builtin-positions
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-15196-s2-builtin-positions

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Part of #15196
Clause-②: yes (narrowing)

Read-shape change. Two doors now answer six more position names, platform_admin, org_owner, org_admin, org_member, everyone and guest, in all three postures measured (single, single with the Default Organization, walled). The metadata door's GET /api/v1/meta/position lists them beside the stack's own positions (booted showcase: 10 names before, 16 after). S1's catalog read, createSecurityCatalogReader in @objectstack/core, lists them from the engine registry, with source: 'registry' and packageId: 'com.objectstack.plugin-security' (showcase: 10 entries before, 16 after). A client can now also read each one's definition: GET /api/v1/meta/position/:name answers 200 with name, label, description and the package provenance, where it answered 404 RESOURCE_NOT_FOUND. A definition carries identity and display only. Activation, the default flag, the managed_by provenance and the permission-set bindings stay on the sys_position rows and their junction, and those are unchanged.

Why the arm reads narrowing, where the dispatch named widening. Registering the six as a code package's items also narrows one write door. A platform administrator's PUT /api/v1/meta/position/:name naming one of the six answered 200 (saved as an environment-wide definition) and now answers 403 NOT_OVERRIDABLE: the name is provided by a package, and position has no overlay. That is the package provenance the stage asked for, the same provenance the manifest stamps on the platform's permission sets, doing what the metadata door does for every packaged item of a non-overridable type. Both facts are true, so the line names the narrowing arm, and the changeset is declared breaking with an ADR-0087 disposition (no-migration-prescription). This deviates from the dispatch, and the contract review decides it. A name that is not one of the six, and every other door, answer as before.

The rest of #15196 stays open: this is stage S2 only. No reader moves to the catalog read here (S8), and nothing in the position read, the grant columns or the seeders' other writes changes.

What changed

  • One list of the six (builtin-positions.ts): the four identity names with their label and description from @objectstack/spec (BUILTIN_IDENTITY_NAMES, BUILTIN_IDENTITY_METADATA), then the two audience anchors (AUDIENCE_ANCHOR_POSITIONS, i.e. EVERYONE_POSITION and GUEST_POSITION) with the anchor text that lived in bootstrap-builtin-positions.ts, moved verbatim. PositionSchema accepts every one of them.
  • The route: the engine registry's own seam, not the manifest key. Measured first: the manifest's positions key reaches no reader. The engine's stack-collection loop has no positions entry (a waived row in check:stack-collection-maps), and ManifestSchema declares none. ObjectQL.registerApp with a positions and a permissions list registered the permission set under the package id and no position at all. On a booted showcase, a positions list on this plugin's manifest reached neither the engine registry, the metadata service, the metadata door nor the catalog read. So SecurityPlugin.start registers the six through registry.registerItem('position', item, 'name', SECURITY_PLUGIN_ID), on the engine handle start() already holds, before kernel:ready runs the seeders. That is the provenance the manifest stamps on permissions. If the engine exposes no registration seam, it says so once at warn.
  • bootstrapBuiltinRoles reads that list and has no list of its own. The rows it writes are byte-identical: same names, same organizations, managed_by: 'platform', active: true, is_default: false, same labels and descriptions.
  • The declared-positions seeder: the six are taken out of its two-step, nothing else. Measured on main plus the registration alone: the seeder reads the engine registry first and the metadata service only when the registry holds no position, so the six registered names made the registry answer every boot, and the stack's positions (which only the metadata service holds) stopped seeding. The fix is the smallest one that keeps the census identical. The registry counts as holding a position only when it holds one besides the six, and the six are filtered out of whatever the seeder then reads. The two-step itself is untouched, so the seeder's behaviour is identical to main in every state, including a registry that holds a door-authored position.
  • Two pins outside plugin-security follow the new read shape (patch round, 4e969bae6e; declared cross-lane on the CLI lane post in comment 6051162186, and S2's surface amended on feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196). S1's showcase pin security-catalog-showcase.dogfood.test.ts expects the six beside the stack's positions, and the runtime row in standalone-stack-seeder-declaration-copy.test.ts expects the registry's positions to be exactly the six built-ins, none of the stack's (its title follows). Both read the six from @objectstack/spec (BUILTIN_IDENTITY_NAMES, AUDIENCE_ANCHOR_POSITIONS); no exact-set row is weakened. Measured: 31 of 31 and 13 of 13.

Pins

All in builtin-positions.boot.test.ts: a real boot of this plugin (init, start, then its kernel:ready handlers in order) over a real ObjectQL engine on SQLite, with a stack-declared position in the metadata service. Four scenarios: single with no organization, single with an organization and its memberships, walled with an organization at boot plus one created after it, and walled with a door-authored position already in the registry. The goldens were recorded by running this file against the sources at 51290bca2c (the built-in seeder's own code list, main's declared seeder, no registration). Every census, ledger, grant and refusal reading there equals the reading after the change.

  • P2.1: the catalog read lists the six, @registry and owned by this plugin, beside the declared position, in all four scenarios. At 51290bca2c it listed the declared position alone.
  • P2.2: the sys_position census, read as (name, organization, managed_by, active, is_default, label, description), and the write ledger (every sys_position insert and update the boot makes, refused ones included, with the provenance written) are identical before and after.
  • Grant equivalence: the resolver's whole envelope for the platform administrator, the organization administrator, a member and an agent, in single and walled, equals the golden. field_default reaches every human principal through the everyone binding.
  • Position write refusals: a tenant's organization administrator (non-system data-door write) is refused a dangling position name (VALIDATION_FAILED, position:reference_not_found), deleting the everyone row (PERMISSION_DENIED 403) and relabelling the platform_admin row (PERMISSION_DENIED 403). Controls: creating an ordinary position and assigning it both succeed.

Ablations. Each one went through scripts/ablation-replace.mjs: the anchor hit 1 to 0 and the blob changed; after the run the blob equalled HEAD and git diff HEAD was empty.

ablation file red
drop everyone from the declaration builtin-positions.ts 12 of 12
P2.2 ablation 1: the two-step decides on the unfiltered registry bootstrap-declared-positions.ts 3: census and ledger in the three fresh-boot scenarios, the stack's position no longer seeded
P2.2 ablation 2: the seeder's result keeps the six bootstrap-declared-positions.ts 1: the door-authored scenario's ledger, which gains insert REFUSED for the four identity names, insert everyone and insert guest with no provenance, and the built-in pass's restamp update everyone description,label,managed_by managed_by=platform; the final rows are identical, so only the ledger sees it
the dangling-name predicate admits every name position-catalog-refusal.ts 2: the refusal pin, both postures
built-in rows stamped admin instead of platform bootstrap-builtin-positions.ts 6: census in four scenarios; refusals in both postures (the delete turns DELETE_RESTRICTED 409, the relabel succeeds)

Measured on a booted showcase (three postures, same worktree)

Before means plugin-security built from the sources at 51290bca2c; after means this branch.

reading before after
sys_position rows (name, organization, managed_by, active, is_default) 16 / 16 / 32 identical
GET /meta/position 10 names 16 names
catalog read, position 10 entries, all @metadata 16 entries, the six @registry
GET /meta/position/everyone, /org_owner 404 RESOURCE_NOT_FOUND 200, the definition
PUT /meta/position/everyone, /platform_admin (platform admin) 200, saved env-wide 403 NOT_OVERRIDABLE
PUT /meta/position/zz_s2_control (control) 200 200
DELETE /meta/position/guest, nothing stored 200, nothing deleted 200, nothing deleted (message differs)
data door: dangling assignment, delete everyone, patch platform_admin, create org_admin, create a control 400, 403, 403, 400, 201 identical

Verification (at 85b5c7df04 unless stated)

  • plugin-security: vitest run 174 files, 3686 passed, 45 skipped (at a8fae6d146; the later commit touches only the boot test, re-run green: 12 of 12); typecheck exit 0, test layer 0 errors.
  • core: 78 files, 2192 tests passed; typecheck exit 0.
  • Closure built with turbo (@objectstack/plugin-security..., then the dogfood closure).
  • Dogfood, 27 files (the 21 that boot a walled posture, S1's showcase pin, and six position/baseline files): 25 files green, rls-multitenant skipped by its own organizations probe, and S1's showcase pin red on 3 rows before the patch round (31 of 31 green at 4e969bae6e).
  • Gates: the 65 commands dispatch-gates --commands derives for this diff, run at 85b5c7df04: all 65 exit 0, reconciled with --ran (65 derived, 65 run, NOT-MEASURED a derived zero). On the first pass, check:dual-build-cjs-loads answered PREREQUISITE NOT MET (eight unbuilt packages outside this diff's closure) and passed once they were built, and check:query-options-erasure caught four as any options on find() in the new test (test surface 236 to 240), typed in 85b5c7df04 and back at 236.

Two pins outside the original surface, updated in the patch round

Both asserted the pre-S2 read shape and went red with the six in the engine registry. They lay outside the stage's first declared surface (plugin-security only); the seat declared both (6051162186) and the patch round edited them as below.

  • packages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts (domain:cli): position: lists exactly the declared names derives the expected set from the stack's positions only, so all three postures list six names more than it expects. Its door-parity rows stay green: the read and the door agree on 16 names. The edit adds the six, read off @objectstack/spec's constants, to its declared set.
  • packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts: the row at :285 asserts the engine registry holds no position at all with SecurityPlugin booted; it now holds the six. The edit asserts the registry holds exactly the six and none of the stack's.

Acceptance notes

  • A position a metadata author saves through PUT /api/v1/meta/position/:name still silences the stack's declared positions for every organization created after it (walled): the declared seeder's registry-first either-or, unchanged here on purpose. Measured identical before and after: the late organization got the six and the authored position, and none of the showcase's ten. Carried in feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 as stage S2b (seat review 6051173832): the declared-positions seeder reads through S1's catalog read.
  • S1's module doc in security-catalog.ts records a measured table (positions: engine registry 0) taken at 3d9188502e; after this stage the engine registry holds the six. It is a dated measurement, not a false claim. Noted for whoever next edits that seam.
  • skip-changeset does not apply: @objectstack/plugin-security publishes dist, and the changeset is minor.

Generated by Claude Code

claude added 9 commits October 8, 2026 00:42
… metadata (wip)

ADR-0131 C2 stage S2. The four identity names and the everyone/guest
anchors are declared once (builtin-positions.ts), registered with the
engine registry under the plugin's package id, and read by the built-in
seeder; the declared-positions seeder unions its two sources and skips
the six.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…tions stage

Four principals in two postures, through the plugin's real kernel:ready
bootstrap, held to a golden recorded against the pre-change sources.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…ared seeder's input

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…p, with the six built-ins out of its decision and its result

Replaces the union read: the registry-first / metadata-service fallback is
unchanged, and the six declared built-in positions neither count as the
registry holding a position nor reach the seeding loop.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…ns stage (goldens pending)

Catalog listing, sys_position row census and write ledger, grant envelopes
and tenant position-write refusals, over four boot scenarios. Replaces the
grant-equivalence file. Goldens are recorded from the pre-declaration tree in
the next commit.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…pre-declaration tree

Recorded by running this file against the sources at 51290bc (the
built-in seeder's own code list, main's declared-positions seeder, no
registration); every census, ledger, grant and refusal reading there equals
the reading after the declarations. The catalog listing is the one widening:
before, it listed the stack's position alone.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
… declared metadata

Clause-② yes (narrowing): two metadata-door reads widen, and an in-place PUT
of one of the six names at the metadata door is now refused.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…ed query options

Four find() calls had their options erased to any, growing the query-options
erasure ratchet's test surface; the signatures infer without it.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 14 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via bootstrapDeclaredPositions (symbol, a top-level function))
What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ef1fcb26a24ff3842e00226186477db8f4850fbf → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 888a8d258c03e26146593d2ed112b1c164adfb24 — the merge of head 4e969bae6e92673ea6c064c315c64fe3426e4b8d into base ef1fcb26a24ff3842e00226186477db8f4850fbf, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 888a8d258c03e26146593d2ed112b1c164adfb24 && git checkout 888a8d258c03e26146593d2ed112b1c164adfb24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ef1fcb26a24ff3842e00226186477db8f4850fbf 4e969bae6e92673ea6c064c315c64fe3426e4b8d && git checkout -B drift-repro ef1fcb26a24ff3842e00226186477db8f4850fbf && git merge --no-ff 4e969bae6e92673ea6c064c315c64fe3426e4b8d

node scripts/docs-audit/affected-docs.mjs --json ef1fcb26a24ff3842e00226186477db8f4850fbf

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ef1fcb26a24ff3842e00226186477db8f4850fbf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…n-security now declares

S1's showcase catalog pin expects the six beside the stack's positions, and
the runtime registry-copy row expects the registry's positions to be exactly
the six built-ins and none of the stack's; both read the names from
@objectstack/spec. Declared cross-lane on the CLI lane post.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4e969bae6e92673ea6c064c315c64fe3426e4b8d
Local-runs: none

Inputs: card #15196 (body and all 30 comments, rulings 6028868506 and 6050490870 included), PR #22139 (body, 11-file list, net diff against main at this head), and the head's check-runs, all through the REST API; source semantics read off the local clone (033e5c536d) without building, running or checking anything out. Check-runs read at 2026-10-08T03:26Z: 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke — path-filtered or opt-in), none failed, none pending. The seven required contexts are all success. No governed path in the file list; +1,076 / −41 lines. Classes, positions and functions only.

① Derived judgments

The diff registers the six built-in positions (platform_admin, org_owner, org_admin, org_member, everyone, guest) with the engine registry as position metadata owned by com.objectstack.plugin-security, from one list (builtin-positions.ts), and reads that same list in the built-in seeder. Every accept-set and public-surface change it implies, each judged:

The widening arm — what a client can newly read. RIGHT.

  • SecurityPlugin.start calls registerBuiltinPositions(ql.registry, SECURITY_PLUGIN_ID), which calls SchemaRegistry.registerItem('position', copy, 'name', packageId) for each of the six. registerItem stores them under the composite key com.objectstack.plugin-security:name and stamps _packageId (applyProtection), i.e. as artifact-backed packaged items — the same shape the manifest registration gives the plugin's permission sets. Registration happens on the engine handle start() already holds, before kernel:ready.
  • Consequences, each read off the repo and pinned in the diff: GET /api/v1/meta/position lists the six beside the stack's positions (the door reads the registry); GET /api/v1/meta/position/:name answers 200 with name, label, description and package provenance where it answered 404; S1's createSecurityCatalogReader.list('position') lists them with source: 'registry' and the plugin's package id (registry-first read). Pins: P2.1 in four boot scenarios, the showcase dogfood pin, the runtime registry-copy row; the Dogfood Regression Gate and Test Core are success on this head.
  • What a definition carries: identity and display only (BuiltinPositionDeclaration is name, label, description; PositionSchema.safeParse pinned for all six). Activation, is_default, managed_by and the permission-set bindings stay on sys_position and its junction — right, and consistent with decision: ADR-0131 C2/C3 order — C2 must read positions' permission sets, which only C3 adds, while §8 makes C3 wait on C2. Which way is the knot cut? #22006 ruling B (no permissionSets key, PositionSchema untouched).
  • A read-side consequence the PR did not name but which follows and is correct: the lock envelope the metadata read publishes (getMetaItem's derivation through packagedBaseRefusal) now reports the six as not editable at the door — the same fact as the narrowing below, seen from the read side.

The narrowing arm — what is newly refused. RIGHT.

  • PUT /api/v1/meta/position/:name naming one of the six. On main no artifact backed the name, so the write took the runtime-only intent and position's allowRuntimeCreate: true admitted it (SysMetadataRepository.assertAllowed). After this diff isArtifactBacked resolves through getArtifactItem, which scans the composite entries for a genuine _packageId and finds the packaged item, so the intent is override-artifact; position is allowOrgOverride: false (the rolled-back security row in DEFAULT_METADATA_TYPE_REGISTRY) and has no ADR-0126 regime row, so refusePackagedBaseOverride (environment kernel) and assertAllowed (host-config kernel) throw NOT_OVERRIDABLE 403 with the ADR-0005 sentence, on every topology. This is exactly the treatment the plugin's packaged permission sets already receive. The refusal reaches only names a package ships: the predicate reads the registry, not a list, and a control name keeps 200.
  • DELETE /api/v1/meta/position/:name on one of the six is NOT narrowed, and the changeset says so correctly: refusePackagedBaseRemoval refuses only when the item is artifact-backed AND the type has no overlay channel AND the type does not merge an overlay at read; position is supportsOverlay: true, so the removal carve-out applies (the same shape as page): nothing stored means 200 and nothing removed; a stored row would be removed, as before.
  • The data door is untouched by the diff: the dangling-name refusal (position-catalog-refusal.ts), the platform-provenance delete and relabel refusals, and a control create and assign are pinned identical (REFUSAL_GOLDEN, both postures).
  • Grants: no resolver reads a definition yet (S8 has not landed), the rows and the junction do not move, so every principal's envelope is identical (GRANT_GOLDEN, single and walled, four principals). Right.

Row writes — RIGHT, and the no-change claim holds where it is pinned.

  • bootstrapBuiltinRoles iterates securityBuiltinPositions in the order it always wrote (four identity names from BUILTIN_IDENTITY_NAMES and BUILTIN_IDENTITY_METADATA, then everyone, guest with the anchor text moved verbatim) and writes the same columns; the census and write-ledger goldens recorded against the pre-change tree are equal in four scenarios, and the audience-anchors pin asserts the seed equals the declaration list.
  • bootstrapDeclaredPositions now reads through readDeclaredPositions: the registry counts as holding a position only when registered.some(isSeededHere), and the result is filtered by isSeededHere (isBuiltinPositionName, exact match). So with only the six registered the metadata service is still read and the stack's positions keep seeding; in the door-authored state the registry still answers alone, minus the six. The six therefore keep one writer. The seeder order is unchanged (bootstrapDeclaredPositions near security-plugin.ts:4396, then bootstrapBuiltinRoles near :4421), which the ledger order pins.
  • The registry-absent path returns 0 and start() logs once at warn, naming the consequence (the door and the catalog read will not list the six; rows and grants unchanged). That is a functional degradation and warn is the right level; nothing is recorded that a later registration in the same boot could contradict.

Public package surface — RIGHT. packages/plugins/plugin-security/src/index.ts is not in the diff and re-exports nothing from builtin-positions.ts; the package exposes only its root entry. No new export of @objectstack/plugin-security, no packages/spec change, no authorable key. The changeset's "no authorable key, export or stored shape changes" holds. The two test edits outside the plugin (packages/qa/dogfood, packages/runtime) publish nothing.

Alignment with the rulings. Q1 = A (seat verdict 6039027082) is what this diff does. Q4 = A (6050490870) names the built-ins as holders of the position namespace that #22135's install-time refusal must honour; after this diff the six carry package provenance, which is what such a refusal can read (S1's finding that stack-declared positions carry no _packageId no longer applies to the six).

② Semver level

  • Changeset .changeset/15196-builtin-positions-declared-metadata.md: @objectstack/plugin-security: minor, a ! title, a BREAKING banner, the declaration line, and the adr-0087 marker not-required (no-migration-prescription).
  • main carries no .changeset/pre.json (read at review time), so the launch-window convention stands: a breaking change ships minor with its banner and disposition. check-changeset-no-major reads a declared narrowing as breaking-at-minor and check-adr-0087-registration reads the arm off the changeset; both ran green on this head (Lint & Repo Gates, Check Changeset).
  • Level: the widening alone (six new 200 answers, six more listed names, a new catalog-read population) takes at least minor; the narrowing is breaking. minor plus the banner is the right grade. Only @objectstack/plugin-security moves source, so no other changeset is owed; skip-changeset does not apply because the package publishes dist. RIGHT.
  • Disposition: nothing an author writes is removed or renamed (no spec key, no export, no config field), no stored row is rewritten, and the changeset body ships no rewrite instruction — "give an authored position a different name" is authoring guidance for a new item, not a from-to rewrite. no-migration-prescription is the honest category. RIGHT.
  • Clause-②: line. The PR body's second line reads Clause-②: yes (narrowing): the fixed key, a valid token, an arm from the closed pair, and the changeset carries the same line, so the two gates read one declaration. In the repo's spelling (scripts/pm/clause2-line.mjs) yes (narrowing) is "a diff that widens one surface and narrows another — both facts are true and both are read". Judged on both arms in ①: the read surface widens (value yes), one write door narrows (arm narrowing). RIGHT. The dispatch's yes (widening) would have hidden the breaking fact from the gate that exists to see it; the dev's deviation is the correct declaration, and the seat corrected its own claim line publicly (6051166176).

③ Boundary flags

Dev flags (PR body; report 6051130805), each answered against the diff:

  1. Q1, the arm deviates from the dispatch — answered A on the card (6051173832, 6051166176). Holds: ① confirms a real narrowing at one door from the door's own predicate, and ② confirms the declaration, grade and disposition that follow from it.
  2. Q2, two pins outside the first surface — answered A: both prepared edits are in this head. The dogfood pin's DECLARED.position gains the six read off @objectstack/spec (BUILTIN_IDENTITY_NAMES, AUDIENCE_ANCHOR_POSITIONS) and still derives its expected set from producers; its door-parity rows stand. The runtime row moves from "no position in the registry" to "exactly the six, none of the stack's", with its title following — strictly stronger. No exact-set row is weakened; S2's surface was amended on the card (6051166176). The patch commit is what moved the head to this sha, and Test Core and the Dogfood Regression Gate are success on it. Holds.
  3. Class-a finding: a door-authored position stops stack-declared positions seeding into organizations created later (walled). Unchanged by this diff, by the stage's no-other-row-write rule; carried on the card as stage S2b (6051173832), matching the carrier in 6039027082. The diff neither fixes nor worsens it — the door-authored scenario's census and ledger are pinned identical. Carried correctly.
  4. security-catalog.ts's measured table predates S2 — carried to S8a. A dated measurement, not a false claim. Noted.
  5. skip-changeset does not apply — right (②).

Reviewer's own flags:

  1. One unpinned state moves, in the repair direction. A deployment that saved an environment-wide definition under a built-in name through the door before this change: it is hydrated under the bare key, so at read it still shadows the packaged artifact (ADR-0005 overlay rule; registerItem logs the artifact-vs-DB collision), it is now locked against PUT but still removable (the carve-out), and readDeclaredPositions now filters it out of the declared seeder where main fed it into a refused or restamped row write and let it silence the stack's positions for later organizations. After this diff those positions seed again and the stored body never reaches a row. No stored row is rewritten, so the disposition holds; but this fifth state is outside the four pinned scenarios. Not a blocker; S2b or S7 is the place to pin it.
  2. No scripts/adr-anchors/ entry for the new D2 implementation site (builtin-positions.ts and the start() registration); the ADR id is left in the code. check:adr-anchors is green because it verifies existing anchors. The owning seat decides whether this stage or S8 carries the anchor. Not a contract failure.
  3. Nothing else is open: the dev's report lists exactly the two questions above, both answered on the card, and the seat's review names no further flag. The ruled questions Q3 and Q4 are S8's and feat(objectql,metadata,runtime)!: refuse a package whose position, permission set or capability name is already held by an installed package, the environment catalog or a built-in (ruling Q4 = A on #15196; narrows ADR-0048 §3.4) #22135's, not this stage's.

Implemented-by: claude/issue-15196-s2-builtin-positions
Reviewed-by: session_01WMQprn46CND82KmY8sZWBu

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants