Skip to content

feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) - #8078

Merged
huangyiirene merged 8 commits into
mainfrom
claude/issue-7990-inline-credential-refusal
Aug 12, 2026
Merged

huangyiirene merged 8 commits into
mainfrom
claude/issue-7990-inline-credential-refusal

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Part of #7990 — the spec half only of the maintainer-ruled Option A split (comment 5266068845, 「接受你的全部建议。」). The services write/read-path half (including the false "credential-stripped" claim at datasource-admin-service.ts) and the existing-cleartext-rows migration are deliberately NOT here; the PM files them as Blocked-by: sub-cards at ACCEPT per the claim comment (5266425167). Merging this PR must not close the card.

What changes

Driver config family (postgres / mysql / mongo / turso). config.password and turso's config.authToken become declared-unwritable: the key stays in the shape as z.never() carrying the refusal prescription, so the removal is audible in tsc (input type never), in the parse (message names the key, the datasource secret binder → sys_secret, and external.credentialsRef — never a bare unrecognized_keys), and in the authorable-surface ratchet ([RETIRED] flip + 4 registered retired-key entries). Former alias spellings (passwd/pwd, turso token/jwt/auth_token/authtoken) carry the refusal directly via guidance — an alias row pointing at a tombstoned key would be the documented two-step-rejection trap.

Why tombstone instead of deleting the key: the Studio connection form renders its masked secret input from the format: 'password' marker in the JSON-Schema projection and routes the value to the top-level secret (the binder's door, never config). A hand-tailored never-key keeps { "not": {}, "format": "password" } in the projection, so the wizard keeps the very input the refusal diverts authors to. Pinned both halves in driver-credential-refusal.test.ts.

Connector authoring door. DeclarativeConnectorEntrySchema (behind defineStack({ connectors }) and PUT /meta/connector/:name) now refuses non-none authentication on every authored entry — catalog descriptors included; previously only provider-bound instances were covered (ADR-0097 §3), which was the ①-d hole. The runtime shape is untouched: plugins handing resolved secrets to registerConnector (slack/rest/openapi/mcp connectors, engine.ts:1948/2027) keep working, and shared/connector-auth.zod.ts now documents the runtime-vs-authored split explicitly.

Ledger. 4 retired-key entries + 2 D3 semantic entries (structured TODOs). Deliberately no D2 conversion: a cleartext credential cannot be mechanically rewritten into an encrypted sys_secret row at load — auto-deleting the key would silently drop a live credential. Regenerated: migration registry, spec-changes.json, upgrade guide, authorable-surface, api-surface, export-origins, reference docs.

Changeset bump reasoning

@objectstack/spec: major, following the acceptance-narrowing precedent (#4583 datasource-capabilities-retired: authorable-key removal = major + FROM → TO + registered ledger disposition; same route here, with semantic entries instead of a conversion because no lossless mapping exists). @objectstack/example-showcase: patch for the migrated descriptor. ADR-0087 disposition: registered datasource-config-inline-credential-refused, connector-inline-authentication-publish-refused — gate verified green on this diff.

Census (dispatch-mandated): in-repo sites writing an inline credential today

  • examples/app-showcase/src/system/connectors/index.ts ErpCatalogConnector — descriptor with placeholder authentication: { type: 'api-key', key: 'SET_AT_INSTALL_TIME' } → migrated (auth scheme moved to description prose; comment documents the instance form with auth.credentialRef).
  • packages/spec test fixtures (postgres/mongo/turso/datasource tests) → triaged individually (re-spelled credential-free, or replaced with refusal pins; the ${DB_PASSWORD} placeholder tests re-scoped to non-credential keys with a measured note: nothing resolves ${…} placeholders — they reached the client verbatim).
  • packages/qa/downstream-contract DcConnector (oauth2, env: strings) — deliberately NOT migrated: it pins ConnectorSchema, the runtime shape that legitimately carries resolved secrets; migrating it would weaken the runtime-shape pin.
  • No example/app writes an inline datasource credential; the showcase's commented warehouse example already models external.credentialsRef.

Verification (real numbers; consumer sweep direction = DOWNSTREAM consumers of @objectstack/spec, enumerated by name)

  • spec: full suite 10115 tests / 382 files green (after triage), typecheck + test-layer typecheck green, check:generated all 13 current on the merged tree.
  • Consumers: service-datasource 339, objectql 3356 (includes the /meta door 422 envelope pins), runtime 2171, metadata 603, service-automation 940, platform-objects 347, connector-rest/slack/mcp/openapi 16/8/23/32, downstream-contract 14 — all green. Examples validate: crm / showcase / todo all ✓.
  • Gates: check:driver-conformance, check:adr-anchors, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:doc-formula-expressions, check:docs-audit-scope, check:i18n, check:merge-driver, check:release-body, check:spec-parsed-alias, check:nul-bytes, check:adr-0087-registration — all PASS locally. Strictness ledger and test-typecheck-debt.json untouched (no spec: close memory driver's persistence sub-shapes against unknown keys (#4001 batch B) #7985 collision; its memory-driver face is disjoint).
  • Reverse verification (fix committed first; predicted directions stated before running): restoring the pre-[security] sys_metadata.metadata is a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasource config.password, connector authentication) #7990 postgres limb went red in BOTH predicted channels — exactly the 6 postgres-facing refusal pins failed (23 others stayed green), and the authorable-surface gate failed from the other side (check (b2): "RETIRED_KEYS_BY_MAJOR entry names a key that is still LIVE — data/PostgresConfig:password"). Restore proven byte-identical by git hash-object.
  • Same-day churn: origin/main merged twice (through 22f0daa) via the mechanized os-regen-merge.sh path; both sides' ledger entries verified present after regeneration.

Open questions (recorded, not expanded on — see the report on #7990)

  1. URL-embedded credentials (postgresql://user:pass@host in config.url) are a live, unruled door — measured and pinned as a FACT in driver-credential-refusal.test.ts, not refused (PM mechanism note 3).
  2. turso encryptionKey stays writable: credential-shaped, but neither ruled mechanism (binder secret → password/authToken injection; credentialsRef) can carry it — refusing it would leave local-file encryption unconfigurable.
  3. Managed datasources have no in-contract ref mechanism: external is refused when schemaMode: 'managed' (datasource.zod.ts:550), so a code-defined managed datasource now has no spec-level credential slot at all (the wizard path and URL-embedding remain). Belongs to the services sub-card / maintainer.

Out-of-scope census finding filed as #8075 (two more schemas permitting inline credentials — third-surface input for the parked Option-B reopen trigger).


Generated by Claude Code

claude added 5 commits August 12, 2026 13:39
…onnector authoring door (#7990)

Driver family (postgres/mysql/mongo/turso): config.password / config.authToken
are declared-unwritable (z.never with the refusal prescription) so tsc, the
parse and the connection form's format:'password' secret input all stay wired
to the datasource secret binder (sys_secret + external.credentialsRef). Former
alias spellings (passwd/pwd/token/jwt/auth_token/authtoken) carry the refusal
directly via guidance.

Connector: DeclarativeConnectorEntrySchema now refuses non-none authentication
on EVERY authored entry — catalog descriptors included (was: provider-bound
instances only, ADR-0097 §3). Runtime registerConnector keeps the resolved
inline shape.

Ledger: 4 retired-keys entries + 2 D3 semantic entries (no D2 conversion — a
cleartext credential cannot be mechanically rewritten into an encrypted
sys_secret row); regenerated registry, spec-changes, upgrade guide, authorable
surface ([RETIRED] flips), api-surface, export-origins, reference docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
… descriptor migration (#7990)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
…egen step 4)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
@vercel

vercel Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 12, 2026 3:10pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

⛔ 7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ource read path, and fix the false "credential-stripped" claim (objectstack-ai#8081) (objectstack-ai#8126)

`getDatasource()` returned the driver `config` verbatim while its own doc
comment promised "with the credential stripped" and described `config` as
"non-sensitive — credentials live in `sys_secret`, never in config". Nothing
stripped anything. The comment was load-bearing: it is why the gap survived a
26-surface credential survey.

objectstack-ai#8078 closed the WRITE door (`config.password` / `config.authToken` are
`z.never()` on every driver that has them) and deliberately left stored rows
alone. Those rows still hold cleartext, and `GET /api/v1/datasources/:name`
handed it to every caller.

The refused-key set is DERIVED from each driver's contract — objectstack-ai#8078 spells a
refused inline credential as `z.never()`, so the schema is the list — plus the
pre-objectstack-ai#8078 alias spellings (a stored row never met the parse that renamed them)
and turso's still-writable `encryptionKey`. A driver with no shipped contract
keeps the canonical spellings hidden by name.

A credential embedded in `config.url` is redacted too, at the password
component of the userinfo only. Refusing such a URL at the write door stays
UNRULED (objectstack-ai#7990) and untouched: redacting on the way out is not refusing on the
way in, and a scrub that dropped `config.password` while serving the same
secret one key over would be a scrub in name only.

`updateDatasource` carries the hidden material forward across a same-driver
round-trip, after the validation gate rather than before it — the gate judges
what the author wrote, and this is material the author never saw. Without it
the scrub would have turned every untouched "Save" into silent credential
deletion. It also repairs a regression objectstack-ai#8078 introduced and this card measured:
the form was served `config.password` verbatim, posted it back, and the gate
refused it — so editing any legacy datasource answered 400 for a value the
server itself had just supplied.

The stored record is never mutated; the connect path is unaffected.

Reverse verification: 9 of 21 pins fail on `origin/main` (each naming the
defect it carries), 21/21 pass here. The two guard pins — objectstack-ai#8078's refusal with
its guidance intact, and URL credentials still ACCEPTED at the write door —
pass on both sides by design.


Claude-Session: https://claude.ai/code/session_01WCoM9vVXw6yAVgEc4oio4m

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…authToken (objectstack-ai#8152) (objectstack-ai#8188)

After objectstack-ai#8078 a NEW turso datasource could not be authenticated by any route an
author has. objectstack-ai#7990/objectstack-ai#8078 made `config.authToken` a refused inline credential
(`z.never()`) at every authoring door, exactly like the SQL drivers'
`config.password`, and diverted the author to the secret binder: bind the
credential, keep only `external.credentialsRef` on the record. The connect path
resolves that ref and hands the cleartext to the driver factory as
`spec.secret` — and nothing on the turso path read it.
`TURSO_CONFIG_READERS.authToken` consulted `config.authToken` alone, so the
resolved secret was dropped and the connection was attempted unauthenticated.

`authToken` now reads `spec.secret` first and falls back to `config`. That is
exact parity with the postgres / mysql / mongodb arms in this same package
(`spec.secret ? { password: spec.secret } : cfg.password ? … : {}`) — no new
mechanism, no spec change, no second binder slot. `spec` was already on
`TursoConfigSource` for `schemaMode`, so the credential had been reaching this
function all along.

`config.authToken` stays readable, and not only for legacy stored rows: the CLI
and standalone hosts translate `OS_DATABASE_AUTH_TOKEN` / `TURSO_AUTH_TOKEN`
into a `config` they construct themselves, which never meets the authoring
schema that refuses the key. An empty `spec.secret` is unset and falls through
to `config`, matching this builder's existing rule for string keys.

Pins, with their readings on origin/main (a0fdc56):

  RED on main (6)
   - turso-driver-config: bound secret reaches `authToken`; bound secret beats a
     stale inline `config.authToken`; binds to `authToken` only, never
     `encryptionKey`.
   - turso-bound-secret-authoring (new): a datasource created through the real
     admin door with a bound secret arrives at the driver with an `authToken`;
     the bound route is the ONLY one a newly authored datasource has (both
     halves in one case); the credential lands in one slot, not two.

  GREEN on main (15)
   - objectstack-ai#8078's inline refusal still fires at create AND at update, and its
     guidance still names both `external.credentialsRef` and the secret binder.
   - `config.authToken` is still read when no secret is bound (the host env
     route).
   - postgres / mysql / mongodb still read `spec.secret` as the password, and a
     bound secret still beats an inline `config.password` — the sibling arms
     this change takes its shape from, unpinned until now.

The gap was invisible because a stored row bypasses the parse and still
connects (only new authoring was dead) and because `turso-driver-config.test.ts`
had no `secret` case at all. Every red pin therefore starts at
`createDatasource()`, not at an existing record: the vacuous version of this
test is green on main.

`encryptionKey` deliberately untouched — a different secret, one binder slot,
and whether it needs a second is a separate decision. objectstack-ai#8126's read-time
redaction of it is untouched too.

Gates: check-test-source-alias OK (72 packages scanned, 62 registered;
`@objectstack/spec` was already the registry entry for this package, so no
widening); check:type-check-coverage OK (64/77 type-checked); the new test file
is type-checked (package `tsc --noEmit` clean). No refusal surface changed, so
no ADR-0112 envelope.

Tests: service-datasource 16/16 files 376/376 cases, runtime 145/145, cli
115/115.


Claude-Session: https://claude.ai/code/session_01Lw4Dm3qYuWFNJFMwQDdkzv

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…driver config.url at publish (objectstack-ai#8082) (objectstack-ai#8341)

* feat(spec)!: refuse URL-embedded credentials in driver config.url at publish (objectstack-ai#8082)

The objectstack-ai#7990 closure refused the inline credential keys; objectstack-ai#8078 measured and
pinned that config.url still accepted the identical secret one syntax over.
Maintainer-ruled Option A (2026-08-12): one value-level parse
(urlUserinfoPassword / credentialFreeUrl, data/driver/common.zod.ts) shared
by the four URL-bearing driver schemas — postgres/mysql/mongo url, turso
url + syncUrl — refuses a URL whose userinfo carries a non-empty password.

- Bare-user userinfo (user@host) stays accepted, matching objectstack-ai#7990's posture
  (username is a writable key; only the secret is refused) — and it is the
  exact shape the objectstack-ai#8126 read path serves for legacy rows, so an untouched
  Save keeps working.
- The refusal message names the working mechanisms (secret binder /
  external.credentialsRef), states the runtime-DSN carve-out explicitly
  (OS_DATABASE_URL never passes the publish door), and warns that
  ${...} placeholders resolve to nothing (objectstack-ai#8078, measured) instead of
  steering authors into that broken escape.
- The objectstack-ai#8078 acceptance pin (driver-credential-refusal.test.ts) is INVERTED
  to a rejection pin, not deleted; the objectstack-ai#8126 write-door acceptance pin in
  service-datasource flips the same way (its own comment said it waited on
  exactly this ruling).
- ADR-0087: D3 semantic entry datasource-config-url-userinfo-refused (no D2
  conversion — a credential cannot be mechanically encrypted into
  sys_secret); registry, spec-changes, upgrade guide, api-surface,
  export-origins, reference docs regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2

* test(spec): re-spell two driver URI fixtures credential-free (objectstack-ai#8082 fixture triage)

mongo.test.ts / postgres.test.ts 'accept config with connection URI' merely
used the userinfo spelling to demonstrate URI acceptance — re-spelled to the
bare-user form the rule still accepts (disposition: re-spell; the family
rejection pins live in driver-credential-refusal.test.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2

* chore(spec): regenerate artifact projections on the merged tree (os-regen step 4)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…river config keys at publish (objectstack-ai#8336) (objectstack-ai#8457)

* feat(spec): refuse ${…} placeholder syntax in connection-material driver config keys at publish (objectstack-ai#8336)

The objectstack-ai#7990 census (measured during objectstack-ai#8078) established that ${…} placeholders
in authored datasource config are resolved by nothing — stored verbatim in
sys_metadata, handed verbatim to the database client, failing at a distance
with no error naming the placeholder. Two shipped refusal messages (objectstack-ai#8078,
objectstack-ai#8082) had to warn around the broken escape. Maintainer-ruled direction 2
(2026-08-13): refuse the syntax loudly at publish; direction 1 (implement
resolution) rejected — a capability with an env-exfiltration surface and no
measured pull.

- One shared value-level judgement in data/driver/common.zod.ts
  (UNRESOLVED_PLACEHOLDER_REFUSED / containsUnresolvedPlaceholder /
  placeholderFree / placeholderFreeDeep), the objectstack-ai#8082 single-mechanism
  construction, applied to every connection-material string key the shared
  factory hands to the client: postgres url/host/database/username/schema/
  applicationName, mysql url/host/database/username, mongo url/host/database/
  username/authSource + the options passthrough (judged deep), turso
  url/syncUrl/encryptionKey, sqlite + sqlite-wasm filename.
- Boundary: placeholder-by-intent — complete ${…} spans only; $VAR, {name},
  unclosed ${ stay accepted; no-contract drivers stay unjudged (objectstack-ai#4410 line).
- The objectstack-ai#8082 message now points at the refusal instead of warning around it.
- objectstack-ai#7990-census acceptance pins inverted, not deleted (datasource.test.ts,
  postgres.test.ts); family pins in driver-placeholder-refusal.test.ts.
- ADR-0087: D3 semantic entry datasource-config-placeholder-refused (no D2 —
  no mechanical rewrite exists); registry/spec-changes/upgrade-guide
  regenerated. Changeset: @objectstack/spec minor (launch-window accept-set
  narrowing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MNV7ZSCjNfA38eDCjsXQL

* chore(spec): regenerate api-surface for the objectstack-ai#8336 exports

gen:api-surface after rebuilding dist (the migration-registry regen had made
dist older than src, which the generator refuses — objectstack-ai#7122/objectstack-ai#4687). Adds the four
new data exports: UNRESOLVED_PLACEHOLDER_REFUSED, containsUnresolvedPlaceholder,
placeholderFree, placeholderFreeDeep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MNV7ZSCjNfA38eDCjsXQL

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…l-key definition in data (objectstack-ai#8300) (objectstack-ai#8530)

* feat(spec): per-type metadata redaction seam in kernel + one credential-key definition in data (objectstack-ai#8300)

- kernel/metadata-type-redaction.ts: registerMetadataTypeRedactor /
  getMetadataTypeRedactor / listMetadataTypeRedactorTypes, mirroring the
  registerMetadataTypeSchema registry pattern; the datasource redactor is
  wired as a BUILT-IN (plugin-init registration is measured fail-open).
- data/datasource-credential-redaction.ts: the credential-key derivation and
  read-path redaction moved from service-datasource (z.never() contract +
  pre-objectstack-ai#8078 aliases + turso encryptionKey), pinned byte-equal by test.
- service-datasource re-exports the moved names and keeps
  restoreRedactedConfig (write-path inverse).
- api-surface/ + export-origins/ regenerated for the new exports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MNV7ZSCjNfA38eDCjsXQL

* merge origin/main (os-regen artifacts taken from main; regeneration follows)

* chore(spec): regenerate api-surface + export-origins on the merged tree (objectstack-ai#8300 relay)

Post-merge regeneration on a known-good base per scripts/pm/os-regen-merge.sh:
the merge commit took origin/main's side of the os-regen artifacts; this
commit re-derives them from the merged source (5074 exports, data + kernel
shards), discharging the pre-commit deferral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MNV7ZSCjNfA38eDCjsXQL

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…d path, with the write-path carry-forward (objectstack-ai#8154) (objectstack-ai#8673)

* fix(metadata-protocol): redact stored credentials on the metadata read path (objectstack-ai#8154)

The /meta read exits served the whole stored body, so a legacy datasource
row came back with config.password in cleartext on getMetaItems,
getMetaItem and getMetaItemLayered (both overlay and effective layers).

Consume the per-type redactor registry landed by objectstack-ai#8300
(@objectstack/spec/kernel) rather than patching the datasource shape:
datasource is the registry's first consumer, not this code's subject.

_diagnostics stay computed on the RAW stored body BEFORE redaction — the
redacted body is exactly what the post-objectstack-ai#8078 schema accepts, so the
inverse ordering flips valid:false to valid:true and destroys the objectstack-ai#8081
item-3 migration inventory. Composed inside decorateMetadataItem so no
call site can invert an ordering it cannot see.

Ships with the write-path inverse, which is not optional: saveMetaItem
accepts a redacted body and persists the credential away, so the read
scrub alone converts today's loud 422 into silent credential deletion on
an ordinary GET-edit-PUT round trip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH

* docs(changeset): metadata read-path credential redaction (objectstack-ai#8154)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…o sys_secret (objectstack-ai#8155) (objectstack-ai#8699)

Adds `IDatasourceAdminService.migrateCredential(name)` — the migration for
datasource rows written before objectstack-ai#8078, whose credential is still in cleartext
inside `config`. objectstack-ai#8081 and objectstack-ai#8154 closed the read paths; neither removes what is
already at rest.

Shape (b) as ruled: per datasource, initiated by an operator from the Setup
action "Move credential to the secret store", backed by
`POST /api/v1/datasources/:name/migrate-credential`. No batch spelling exists,
deliberately — a boot-time sweep decides a secret's identity with no operator
present.

Durability: bind, read the secret back and compare, then ONE record write that
adds `external.credentialsRef` and drops the inline key together. A crash before
that write leaves the row working on its inline credential; a failed read-back or
record write unbinds the secret just minted rather than orphaning it. Writing the
ref first in a separate step would be worse, not safer: the connect path is
fail-closed on a ref it cannot resolve (ADR-0062 D3).

Idempotency: a row already referencing a secret is never bound again
(`already-bound`, no write); a row holding both a ref and an inline copy has the
copy dropped against the existing ref.

Only the key a driver's own contract declares as its credential slot is re-homed
(`refusedCredentialKeys`) — the key the injected `spec.secret` substitutes at
connect time. URL-embedded credentials, pre-objectstack-ai#8078 alias spellings, turso's
still-writable `encryptionKey`, code-defined rows and hosts without a readable
secret store are REFUSED with a reason and a remedy, never guessed at.


Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… commits that decided them (objectstack-ai#20693)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the fifth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-datasource/src/**` and nothing else. By the
seat's fresh census at the claim (`5894843429`), it is the largest
package in the lane that no in-flight work holds. Later stages cover the
other packages, so this PR says `Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 4 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`). That is **75 sites on 74 lines in 23 files,
covering 16 numbers**:

- 44 census sites (every census site this package has);
- 31 sites in test comments, which the census defers.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **17 distinct shas**. `objectstack-ai#8696` was one card fixed in two halves,
so its lines cite the half they describe: the mysql DSN branch
(`72050cc47`) or the mongodb DSN branch (`90a12fb18`). `PR objectstack-ai#8588` was
itself a pull request, and it now cites its squash commit `3dede582b`.
No number in this package has an ADR or ruling record of its own in the
repository (a grep of `docs/adr/` for all 16 finds none), so every
anchor is a commit, per ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(78 lines out, 78 in, over 23 files), so no line citation into these
files moves. 4 of those 78 lines hold no dead citation; they are reflow,
listed under Wordings below. No code token moves (see the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces: the only one is `objectstack-ai#12482`, which
resolves and stood on `datasource-connection-service.ts:101` before.
Over the whole diff, added minus removed is 0 or negative for every
number, and no number is new to the diff. No PR number stands on an
added line.

Thirteen dead sites are left on purpose, all of them test titles (see
the list below).

One more file: a `patch` changeset for
`@objectstack/service-datasource`, because the rewritten docblocks ship
(see Changeset below).

## Census: `service-datasource`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/services/service-datasource/`. Each run counts as a reading
only because its board frontier equals the newest issue number, read by
a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
service-datasource sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `6981abfd2`, run 2026-09-29T17:02:34Z to 17:06:27Z |
enumerated, 186 pages, frontier objectstack-ai#20684 (newest objectstack-ai#20684 before and after),
18,511 numbers | 1,318 | **44** | 43 | 9 | 14 |
| after | head `f5ec6bacd`, run 17:18:37Z to 17:22:33Z | enumerated, 186
pages, frontier objectstack-ai#20686 (newest objectstack-ai#20686 before and after), 18,513 numbers
| 1,274 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim (44 sites in 9
files, at `6bff748b`). The whole-repo drop is 44, exactly this diff's
census sites. The `resolves` tally is 32,909 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (994) did
not move either. The after run was taken on `f5ec6bacd`; the head
`265dc6861` adds only the changeset. No run was truncated or discarded:
all four enumerations in this stage (two census runs and the two
supplementary boards below) read 186 pages at the newest frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `classifyCitation` over
every `.ts` file under `service-datasource/src` (61 files). It uses one
board for both trees, enumerated by the gate's own `enumerateBoard` at
17:22:42Z (186 pages, frontier objectstack-ai#20686, equal to the newest).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `6981abfd2` | 791 | **88** | 44 | 31 | 0 | 13 |
| after, `f5ec6bacd` | 716 | **13** | 0 | 0 | 0 | 13 |

Its src-comment column equals the census's 44, which is the control on
the second instrument. The 646 resolving and 57 pull-request citations
are the same in both readings, and the drop of 75 citations is exactly
the rewritten sites. An earlier board (17:07:08Z, frontier objectstack-ai#20685) gave
the same base reading. A third, raw reading (every `#` followed by
digits, judged against the same board, whatever surrounds it) finds 88
dead occurrences before and 13 after, and its residue equals the gate's
residue site for site.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#6268` | 6/4 | 6/0 | `68f5eccb1`: the libSQL/Turso host loader gets
one owner (`@objectstack/runtime`), and `MissingDriverPackageError`
becomes one class across both hosts, because `serve.ts` decides fatality
with `instanceof`. The cli and runtime stages' anchor |
| `objectstack-ai#6345` | 9/5 | 9/0 | `e2798fab7`: one driver vocabulary; `mongo`
renamed to `mongodb`, `turso` made a full builtin with a contract, and
this factory's dispatch made exhaustive. The spec stages' anchor |
| `objectstack-ai#8588` | 1/1 | 1/0 | `3dede582b`: `external.credentialsRef` (and only
it) allowed on `schemaMode: 'managed'`; `objectstack-ai#8588` was that pull request,
and this is its squash commit |
| `objectstack-ai#8696` | 13/4 | 10/3 | two halves: `72050cc47`, a bound
`credentialsRef` reaches the mysql client on the DSN branch as `{ uri,
password }` (5 sites); `90a12fb18`, the mongodb DSN branch carries it in
`options.auth` beside an unmodified url (5 sites). The spec stage's
anchor for the mongo half |
| `objectstack-ai#8873` | 8/3 | 7/1 | `096106522`: a bound `credentialsRef` reaches
the postgres SERVER on the DSN branch; `connectionString` is dropped and
`pg` gets its own parse of the url with the credential attached. The
spec stage's anchor |
| `objectstack-ai#8874` | 9/2 | 7/2 | `d70428ae7`: a declared `ssl` reaches the mysql
client on both branches, in the spelling `mysql2` accepts (`{}`, never
`true`). The spec stage's anchor |
| `objectstack-ai#8876` | 1/1 | 1/0 | `d634e665b`: `urlUserinfoUsername` exported, the
username half of the shared URL userinfo grammar. The spec stage's
anchor |
| `objectstack-ai#9040` | 4/3 | 2/2 | `24206416a`: a credential in the mongo options
passthrough (`config.options.auth.password`) is refused at publish. The
spec stage's anchor |
| `objectstack-ai#9041` | 2/2 | 2/0 | `d491625c1`: a bound `credentialsRef` with a
user-less mongo `config.url` is refused at the one door that sees both
halves. The spec stage's anchor |
| `objectstack-ai#10537` | 3/2 | 3/0 | `e634ecf6a`: `POST /external/validate` scoped
to the URL's datasource; it adds `validateDatasource`. The rest and
runtime stages' anchor |
| `objectstack-ai#10962` | 5/2 | 4/1 | `29d067646`: one live introspection per
datasource per validation sweep, memoised per call and never per
instance (its message names `objectstack-ai#10962`) |
| `objectstack-ai#11166` | 5/1 | 4/1 | `735f5c709`: an unreachable remote is the new
`unreachable` diff kind, not `missing_table`. The runtime stage's anchor
|
| `objectstack-ai#12010` | 9/5 | 8/1 | `77b91bdb4`: `ConnectionEngineLike` derived
from the engine contract, and `registerDriver` stops promising it
accepts any value. The runtime stage's anchor |
| `objectstack-ai#12248` | 1/1 | 1/0 | `8425c17cc`: the ruled engine members adopted
onto `IDataEngine`, the datasource-lifecycle trio among them. The spec
stage's anchor |
| `objectstack-ai#12943` | 3/2 | 3/0 | `090f2302e`: the guarded optional-driver loads
declared as optional peers of this package. The cli and runtime stages'
anchor |
| `objectstack-ai#13279` | 9/4 | 7/2 | `6a180e42d`: a failed permission-store read
raises `AuthzStoreUnavailableError` (503) instead of reading as zero
grants; its message carries the 2026-08-30 ruling, and it moved
`driver-error-classification.ts` into `@objectstack/types`. The anchor
of stage 2 and of the rest, runtime and types stages |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 17), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 17; the
history is complete, `--is-shallow-repository` false, 15,110 commits).
Where an earlier stage already anchored a number, this stage reuses that
anchor after checking it against this package's lines. New to the sweep
here: `72050cc47` (the mysql half of `objectstack-ai#8696`), `3dede582b` and
`29d067646`.

## Wordings to check

- **`objectstack-ai#8696`'s two halves.** The mysql arm's lines
(`default-datasource-driver-factory.ts:718`, `:824`, and
`bound-secret-dsn-branches.test.ts:4`, `mysql-dsn-ssl.test.ts:189`,
`:263`) cite `72050cc47`; the mongo arm's lines
(`default-datasource-driver-factory.ts:926`, `:954`, `:1243`,
`datasource-credential-migration.ts:182`, and the heading
`bound-secret-dsn-branches.test.ts:72`, 「the mongodb half, added
second」) cite `90a12fb18`.
- **A referent, `datasource-connection-service.ts:95-96`.** 「the
inventory that filed that card」 lost its referent with the number, so it
now says 「the inventory that filed its card」, the card behind
`77b91bdb4` (1 reflow line).
- **`datasource-connection-service.ts:100-101`.** 「objectstack-ai#12248 adjudicated
all three onto IDataEngine」 became 「Commit 8425c17 adopted all three
onto IDataEngine per the ruling」: the ruling decided and the commit
carried it out, as its changeset says (1 reflow line).
- **What the card described, `default-datasource-driver-factory.ts:412`
and `mysql-dsn-ssl.test.ts:40`.** 「the one objectstack-ai#8874 describes as honouring」
became 「the one commit d70428a's card describes as honouring」, since
the words quote the card, not the commit.
- **A cross-reference, `default-datasource-driver-factory.ts:736`.**
「the falsy-value note under objectstack-ai#8874 below」 points at the heading at
`:767`, which now carries `commit d70428a`, so the pointer names the
same anchor.
- **A future tense made past, `postgres-dsn-bound-secret.test.ts:223`.**
「the authoring door (objectstack-ai#9041), which this card lands before」 became
「(commit d491625), which landed after this pin」. `096106522` (this
file's commit) is an ancestor of `d491625c1`, both on 2026-08-16.
- **`external-datasource-service.test.ts:444`.** 「The card's measured
defect」 became 「Its card's measured defect」, the card behind
`735f5c709`; `:588` 「the pre-objectstack-ai#10537 route」 became 「the route … before
commit e634ecf」.
- **`datasource-admin-service.test.ts:674`.** 「Before PR objectstack-ai#8588」 became
「Before commit 3dede58」, the squash commit of that pull request, which
answers 404.
- **Reflow, 4 lines with no dead site** (every file keeps its line
count): `datasource-connection-service.ts:96`, `:101`,
`default-datasource-driver-factory.ts:825`, `:826`.

## The 13 sites left

- **Test titles, 13 sites.** `describe` / `it` titles, which are string
tokens, left as stages 1 to 4 left theirs:
`admin-routes-authz-outage-envelope.test.ts:158` (`objectstack-ai#13279`);
`admin-routes-tenancy-posture-admission.test.ts:557` (`objectstack-ai#13279`);
`bound-secret-dsn-branches.test.ts:136`, `:244` (`objectstack-ai#8696`);
`connection-engine-like-contract.test.ts:21` (`objectstack-ai#12010`);
`datasource-config-redaction.test.ts:406` (`objectstack-ai#9040`);
`datasource-credential-migration.test.ts:226` (`objectstack-ai#9040`);
`external-datasource-service.test.ts:453` (`objectstack-ai#11166`), `:690` (`objectstack-ai#10962`);
`mysql-dsn-ssl.test.ts:165`, `:324` (`objectstack-ai#8874`), `:260` (`objectstack-ai#8696`);
`postgres-dsn-bound-secret.test.ts:160` (`objectstack-ai#8873`).
- There is no operator string, assertion message, generated header or
quoted ruling carrying a dead number in this package. The verbatim
maintainer quotations in scope (「同意」 and 「同意所有」, on 8 lines) carry no
dead number and are untouched.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `6981abfd2` against head.
Template literals are therefore read in context. It ran over all 23
touched `.ts` files.

- Real run: 24,055 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `default-datasource-driver-factory.ts` (`Lazy +
caught exactly like` to `Lazy and caught exactly like`): 0 files
changed, as expected (exit 0).
- Positive control, a code token added in
`default-datasource-driver-factory.ts` (`const url =
resolveTursoUrl(spec);` given a trailing `?? undefined`): DIFFER (exit
1).
- Positive control, one digit changed inside a kept test title
(`mysql-dsn-ssl.test.ts:165`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`8c164aa6178f`, `2feeaeb0411d`), with
`git diff HEAD` empty and a clean tree afterwards. A first draft of the
guard used the bare scanner, which loses template context and reported
token changes inside comments; it was replaced by the parser walk before
any reading was taken from it.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-datasource`
(`.changeset/20596-service-datasource-provenance-anchors.md`) is
included. It says only that the provenance comments were re-anchored, in
stages 3 and 4's words.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten comments reach `dist`:
`6a180e42d`, `e2798fab7` and `e634ecf6a` once, and `29d067646` three
times, in each of `dist/index.d.ts`, `index.d.cts`, `index.js` and
`index.cjs`; `68f5eccb1` 4 times, `090f2302e` twice, and `77b91bdb4` and
`8425c17cc` once each, in both declaration files. Positive control: the
unchanged line 「`registerDatasourceDef`, `markDatasourceUnavailable`,」
beside the shipped rewrite at `datasource-connection-service.ts:95` is
found once in `index.d.ts`. A never-written negative phrase appears
nowhere in `dist`. No dead number of the 16 is left anywhere in `dist`.

## Gates (head `265dc6861`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 1 citation (`objectstack-ai#12482`), and it resolves.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the
sibling-package prose ids at their baseline and no growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `265dc6861` derived 63 commands:
all 54 derived at dispatch, plus `check:duration-unit-keys`,
`check:dispatcher-error-vocabulary`, `check:engine-double-contract`,
`check:logger-receiver-detach`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 63 exit 0. `--ran`, fed each
command with its exit code, reports 63 run, 0 NOT MEASURED (a derived
zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*`
and `./packages/*/*` ran first under the shared verify lock (71 of 71
tasks, exit 0), so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-datasource test`: 34 files pass
and 693 tests pass. That is every test file in the package, the 14
touched ones included.
- `pnpm --filter @objectstack/service-datasource typecheck` exits 0. Its
`tsconfig.json` includes all of `src`, and `--listFiles` shows all 61
files under `src/`, the 34 test files included, and all 23 touched files
in the program.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 23 touched `.ts` files gives 23 files, 0 errors and 0
warnings. All 23 are in eslint's own population (`isPathIgnored` is
false for each). `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, as its own lines 327-328 state), so a
comment edit here cannot move the verdict on any untouched file. The
repo-wide `pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 24 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is no `#N-word` spelling at all. There
are 7 `#A/#B` lines (`admin-routes.ts:28`,
`datasource-route-ledger.ts:159`, `turso-driver-config.ts:132`,
`external-introspection-seam.test.ts:14`, `:102`, `:163`,
`turso-bound-secret-authoring.test.ts:8`), and every second number on
them is live: `objectstack-ai#10998`, `objectstack-ai#4251` and `objectstack-ai#4249` are issues, and `objectstack-ai#8078`,
`objectstack-ai#4176` and `objectstack-ai#4202` are pull requests. So nothing there needed
rewriting. The raw scan above, which sees both spellings, agrees.
- **The census instrument did not truncate in this stage.** Four
enumerations read 186 pages each at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13279` →
`6a180e42d`; `objectstack-ai#12010` → `77b91bdb4`; `objectstack-ai#6345` → `e2798fab7`; `objectstack-ai#6268` →
`68f5eccb1`; `objectstack-ai#12943` → `090f2302e`; `objectstack-ai#8696` → `72050cc47` (mysql) or
`90a12fb18` (mongodb); `objectstack-ai#8873` → `096106522`; `objectstack-ai#8874` → `d70428ae7`;
`objectstack-ai#10962` → `29d067646`.
- **Base.** The branch is 5 commits behind `origin/main` (`14f80e239`,
read at 17:27Z). None touches `service-datasource`, `scripts/` or
`.changeset/config.json`, so there was no merge.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…decision in words instead of a tracker number (stage 19) (objectstack-ai#21882)

Part of objectstack-ai#20749
Clause-②: no

Stage 19 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the last `data/` group: the name-ordered test files
directly under `packages/spec/src/data/` from `query-transport.test.ts`
to `validation.test.ts`, plus `data/driver/`. Those 18 files carried 82
messages and 86 tracker ids, citing 39 records. Every one of those ids
now either states what its record decided, in words (form D), or is
dropped where the title already says it. Text only: no assertion,
identifier, test count or code comment changes. After this PR `data/`
carries no tracker id in a test string.

## Census at the base (`124533b388`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the
copies stages 10 to 18 used. A literal counts as a test title when its
folded message is argument 0 of a `describe` / `it` / `test` call,
`.each` / `.skip` / `.only` chains included. Everything else is an
"other" string.

The base is `124533b388`, the claim's base and stage 18's landing. Both
instruments read **942 messages / 994 ids in 208 files**, the seat's
reading and stage 18's head reading.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `ui/` | 84 | 396 / 419 | 378 / 401 | 18 / 18 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `data/` (this PR) | 18 | 82 / 86 | 81 / 85 | 1 / 1 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **208** | **942 / 994** | **885 / 936** | **57 / 58** |

The group reads **82 messages / 86 ids in 18 files**, the seat's
figures, file for file: 11 files directly under `data/` (34 ids) and 7
in `data/driver/` (52 ids).

| file (under `data/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `driver/config-registry.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `driver/driver-credential-refusal.test.ts` | 27 / 30 | 27 / 30 | 0 |
| `driver/driver-placeholder-refusal.test.ts` | 9 / 10 | 9 / 10 | 0 |
| `driver/memory.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `driver/pg-url-grammar.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `driver/postgres.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `driver/turso.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `query-transport.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `query.test.ts` | 11 / 11 | 10 / 10 | 1 / 1 |
| `record-surface.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `search-fields.test.ts` | 6 / 6 | 6 / 6 | 0 |
| `secret-mask.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `seed.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `tree-reference-self-only.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `unique-scope-message.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `unique-scope.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `validation-boundary-description.test.ts` | 4 / 4 | 4 / 4 | 0 |
| `validation.test.ts` | 1 / 1 | 1 / 1 | 0 |
| **18 files** | **82 / 86** | **81 / 85** | **1 / 1** |

The one "other" string is the `expect` message at `query.test.ts:202`,
declared to the text-only tool. Three more test files sit in the same
name range and carry no id: `seed-loader`, `type-compat` and
`driver/mongo`. They are not touched.

- **Controls.** Lit: `ui/dashboard.test.ts`, outside the group, reads 21
ids at the head as at the base. Dark: `search-fields.test.ts` and
`driver/driver-credential-refusal.test.ts` read 0 at the head while 16
and 29 of their comment lines still carry a number. Planted in scratch
copies of head files: an id put into a `seed.test.ts` title reads 1 / 1,
and an id put into a `secret-mask.test.ts` comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same totals as the
gate pattern in all 18 files at the base, and 0 in all 18 at the head.
- **At the head:** 860 messages / 908 ids in 190 files. The 18 files
read 0 / 0, `data/` reads 0 / 0, and no other file moved.

## How the area was chosen

`data/` has been taken in name-ordered file groups near the ~100-id
bound. Stage 18's re-cut named this group at 86 ids, the whole remainder
of `data/`, and this census reads 86, so the rule needed no re-cut.

**Named for the next stages** (re-cut from the head census, 860 / 908):
- `ui/` 419 ids in 84 files, about four stages. It has no subdirectory,
so the same name-ordered rule applies. The first group near 100 runs
from `action-confirm-params-guard.test.ts` to
`component-record-block-field-security.test.ts`: 32 files, 97 messages /
102 ids. It holds three "other" strings: two `expect` messages in
`action-requires-confirmation-docblock.pin.test.ts` (`:168`, `:175`) and
one in `component-props-unknown-members.pin.test.ts` (`:322`). That
stage should check whether any of them is a needle.
- `api/` 201, two stages. `system/` 165, two. The files directly in
`src/`, 120, one.
- The three docblock needles (`ai/build-progress.test.ts:236`, `:237`,
`contracts/approval-service.test.ts:274`), one stage with their
docblocks.

## What each id became

- **16 literals (17 ids)** now state a decision in words.
- **12 literals (15 ids)** get their subject back in words, where the
number stood for a thing, such as "the objectstack-ai#9041 arm".
- **54 literals (54 ids)** drop a number the title already explains.

Every cited record was read with its comments through REST. 33 answer
200. Six answer 404, and each decision was read from what landed, the
landing commit and its CHANGELOG entry:
- objectstack-ai#6345 (`e2798fab76`, one driver vocabulary: `mongo` → `mongodb`, turso
gets a config contract);
- objectstack-ai#8495 (`4bfe1a539d`, `${…}` refused in memory `persistence.path` /
`persistence.key` at publish);
- objectstack-ai#8696 (`90a12fb18d`, a bound secret injected on the mongodb DSN
branch);
- objectstack-ai#8876 (`d634e665b0`, `urlUserinfoUsername`, the username half of the
userinfo grammar);
- objectstack-ai#9040 (`24206416a7`, a credential in the mongo `options` passthrough
refused at publish);
- objectstack-ai#9041 (`d491625c17`, bound `credentialsRef` with a user-less mongo
`config.url` refused at publish).

**Stated in words:**

| record(s) | literal (under `data/`) | now reads |
|:--|:--|:--|
| objectstack-ai#4410 | `driver/config-registry.test.ts:71` | "DatasourceSchema ×
driver config — parsed against the contract its driver ships". Ruled
enforce: `data/driver/` became the one contract, and `DatasourceSchema`
parses `config` against it. |
| objectstack-ai#6969 | `driver/config-registry.test.ts:178` | "… — what a boot flag
may offer, derived from the one driver table". The CLI's hand-written
`--database-driver` lists were replaced by a set derived from the shared
table. |
| objectstack-ai#8155 | `driver/driver-credential-refusal.test.ts:851` | "accepts the
blessed shape byte-identically: bare-username URL + bound secret, what
the stored-credential remedy prescribes". Ruled shape (b): the operator
moves the credential to the secret store; a URL-embedded one is told to
keep a bare `user@host` and bind the secret. |
| objectstack-ai#8336, objectstack-ai#8495 | `driver/driver-placeholder-refusal.test.ts:234` |
"memory `initialData` stays UNJUDGED — the deliberate seed-data
exclusion holds". The placeholder refusal excluded seed data; objectstack-ai#8495
extended it to `persistence.path` / `persistence.key` only. |
| objectstack-ai#9091 | `driver/pg-url-grammar.test.ts:39` | "pg-url-grammar server
twin — still asks the parser `pg` itself runs". A postgres `config.url`
that `pg` cannot open is refused at publish. |
| objectstack-ai#11072 | `driver/pg-url-grammar.test.ts:55` | "pg-url-grammar browser
twin — degrades to the shape-only checks, with no `pg` parse". Ruled
option A: a `browser` export condition whose bundle drops the `pg`
parse. |
| objectstack-ai#16066 | `query-transport.test.ts:38` | "§1 what the transport
declares — a flattened spelling of the AST, never a second semantics".
Ruled: the wire dialect is declared in spec as a 1:1 alias table;
`QuerySchema` itself does not grow. |
| objectstack-ai#4721 | `query.test.ts:202` (`expect` message) | "this parsed to
`order: asc` before the sort node was closed". Ruled (a)+(b):
`SortNodeSchema` strict, with `direction` → `order` as a prescription. |
| objectstack-ai#2604 | `record-surface.test.ts:59` | "deriveRecordFlowSurface —
viewing a record may route, a create or edit task is always an overlay".
Decision D1: view is a state and may route; create and edit are tasks
and never route. |
| objectstack-ai#2578 | `record-surface.test.ts:64` | "view keeps the field-count
detail surface verbatim: …". The detail surface is derived from the
field count: heavy → page, light → drawer. |
| objectstack-ai#4254 | `search-fields.test.ts:120` | "the ingress gate that refuses
an unsearchable `$searchFields` entry no longer admits
`$searchFields=id`". Ruled: an unknown or unsearchable `searchFields`
entry answers 400, never a wider scan. |
| objectstack-ai#6674 | `search-fields.test.ts:257` | "a virtual field declared in
searchableFields — not admitted, since no driver stores it". Promoted on
the objectstack-ai#4254 precedent: a declared entry that can never match is not
admitted. |
| objectstack-ai#7572 | `secret-mask.test.ts:40` | "SECRET_MASK — the credential read
mask (ADR-0100), declared once for every masked read". Option A: one
declaration in spec, re-exported by objectql and aliased by
service-settings. |
| objectstack-ai#8323 | `unique-scope-message.test.ts:137` | "… the accept/reject line
does not move, and bare `true` keeps its meaning". Ruled: no unannounced
reinterpretation of bare `unique: true`. |
| objectstack-ai#3696 | `unique-scope.test.ts:19` | "UniqueScope (ADR-0120) — bare
`true` on a field is unique per organization; global uniqueness must be
said". Field-level `true` became a composite per-organization index, and
`'global'` the explicit opt-in. |
| objectstack-ai#3184 | `validation.test.ts:1120` | "ValidationRule - events property
— insert and update only; a delete guard is a beforeDelete hook".
Decision: trim `delete` from the enum, not enforce it. |

**Subject back in words** (12 literals, 15 ids). In
`driver/driver-credential-refusal.test.ts` and
`driver/driver-placeholder-refusal.test.ts`, numbers that named a
sibling refusal now name it: "objectstack-ai#9041" becomes "the user-less URL arm" or
"the user-less URL refusal", "objectstack-ai#9147's arm" becomes "the no-username
arm", "objectstack-ai#9040" becomes "the options-passthrough credential refusal" or
"the passthrough read path", "objectstack-ai#8082" becomes "the URL userinfo refusal"
or "check", "the inverted objectstack-ai#8078 pin" becomes "the pin that once recorded
them as accepted, inverted", "objectstack-ai#8336" in "the deep judgement" becomes
"the deep placeholder judgement", and "the honest objectstack-ai#4410 boundary"
becomes "the honest boundary of config validation".

**Dropped where already stated** (54 literals, 54 ids). A number goes
only where the title already says its decision. Examples:
"QueryAST.joins — REMOVED (objectstack-ai#4286)" and its three siblings; "FieldNode —
the nested-select object form is REMOVED (objectstack-ai#4196)";
"AggregationNode.distinct — REMOVED (objectstack-ai#6815, ADR-0049)", which keeps
`(ADR-0049)`; the four `objectstack-ai#13879 —` describe prefixes, each of which
states its semantics; "`$driver` — inline credential refusal (objectstack-ai#7990)"
and the other refusal-family describes; "TursoConfigSchema refuses what
the turso driver refuses (objectstack-ai#19977)". `(ADR-0049)`, `(ADR-0100)` and
`(ADR-0120)` stay: they cite decision records by number, not tracker
ids.

## Readers

- **Test-name filters:** none. No tracked script, workflow or config
passes `-t` / `--testNamePattern`.
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`, and none of the 18 files calls a snapshot matcher.
- **Projects:** none of the 18 files is listed in
`packages/spec/vitest.repo-tests.json`; all 18 run in the `local`
project.
- **By substring:** every old literal, plus a window around each id (245
needles), was searched across the tracked tree outside its own file. No
gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads
one. The 11 hits are:
- 7 code comments citing "the objectstack-ai#4254 ingress gate" (`lint`
`validate-searchable-fields.ts` / `.test.ts`,
`validate-react-page-props.test.ts`, `metadata-protocol`
`protocol.search-title-namefield.test.ts`, `spec`
`data/search-fields.ts:198`);
  - two release-owned CHANGELOG lines;
- a sibling title in this card's `ui/` stage
(`ui/dashboard.test.ts:717`);
- a sibling title in `metadata-protocol`
(`protocol.query-transport-dialect.test.ts:176`).
  None reads a spec test title.
- **The files by name:** outside CHANGELOGs, `data/query.test.ts` is
named by `test-typecheck-debt.json`, which keys on the file and on error
signatures, not on a title, and by two code comments;
`data/secret-mask.test.ts` by one code comment.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares one line,
`query.test.ts:202`.

- **Result:** 18 of 18 files SAME on all three legs, with the per-file
counts predicted in writing before the run.
- **Totals:** 82 changed string leaves in 82 literals: 81 titles and 1
declared. The diff's `+` and `-` lines are exactly the 82 planned lines,
and every file keeps its line count.
- **Controls (10 of 10 as predicted, on scratch copies, each anchor hit
once):** identifier rename DIFF; numeric literal DIFF; comment edit
COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten
title given a new id VIOLATION; a title that was id-free at base edited
VIOLATION; one title reverted to base SAME; an `it.each` row given an id
VIOLATION; an undeclared `expect` message changed VIOLATION; a title
re-split into a `+` chain DIFF.
- **`.each` titles:** five `describe.each` titles lose only their
trailing id. No `$driver` / `$name` placeholder, row or table value
changes.

**Test counts:** the 18 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 737 tests, all passed, with the same count and status
sequence per file in 18 of 18. 476 full test names change, and each
equals the base name with the planned replacements applied (0
mismatches). No full name repeats on either side.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
18 touched files are in it, and no `*.test.ts` at all. The controls
`src/data/query.zod.ts` and `dist/index.mjs` are in it.
- In the built `dist/`, a new phrase and an old literal each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `8788de7a4a`)

- `pnpm turbo run build` over all packages: 71 / 71.
- `@objectstack/spec`:
  - `vitest run --project local`: 616 files, 18426 passed, 1 todo.
- `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 18 touched
files, counted with `tsc --listFilesOnly -p tsconfig.test.json`.
  - `check:generated`: all 15 generated artifacts up to date.
- **Gates:** `dispatch-gates --commands` derived 79 families, the same
set as stages 13 to 18, and all 79 exit 0. `--ran` reconciles: 79
derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit
code recorded.
- The five roster families whose rosters sit under a touched directory
were also run, and each exits 0: `check:meta-url-spelling`,
`check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`
and `check:filter-alias-parity`.
- The reconciliation printed `STALE TREE`:
`scripts/engine-double-contract.pinned.json` moved on `main` after the
base. This diff adds and changes no engine double,
`check:engine-double-contract` exits 0 on this tree, and the queue
re-runs it on the merged generation.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 18 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 18 configured, 0 ignored. No file sets `parserOptions.project`
or `projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 164 changed lines.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was two commits
past the base (`5e0b489bca`: objectstack-ai#21871, objectstack-ai#21873). Neither touches any of the
18 files, and neither touches `packages/spec`. So `main` was not merged.
`git merge-tree` onto `5e0b489bca` is clean. No open PR touches the 18
files.

## Acceptance notes

- **No needle in this group.** The one "other" string is an `expect`
message, not the expected value of an assertion over a source docblock.
The three known needles are untouched.
- **Same-id test titles in this card's later stages** go with those
stages: `api/protocol.test.ts:677` (objectstack-ai#4286), `ui/dashboard.test.ts:717`
(objectstack-ai#15680) and `ui/view-authoring-wire-split.test.ts:192` (objectstack-ai#4721).
- **Same-id test titles in other packages** are their lanes' test-string
shares. A search of `describe` / `it` / `test` lines outside
`packages/spec/src` finds 49 lines citing ids this PR handled, in 12
packages: `service-datasource` 18 (8 files), `metadata-protocol` 7 (4),
`objectql` 6 (4), `cli` 5 (3), `lint` 4 (3), `driver-sql` 3 (3), and one
each in `client`, `driver-turso`, `platform-objects`, `plugin-security`,
`rest` and `service-settings`. Examples:
`cli/src/commands/database-driver-flag-derivation.test.ts:63` ("objectstack-ai#6969 —
…"), `driver-sql/src/sql-driver-unique-tenancy.test.ts:48` ("(objectstack-ai#3696)"),
`lint/src/validate-searchable-fields.test.ts:809` ("[objectstack-ai#6674] …").
- **Code comments still carry ids** in these files and their sources,
for example the `[objectstack-ai#6674]` and `[objectstack-ai#4483]` blocks in
`search-fields.test.ts`, the `[objectstack-ai#16066]` header of
`query-transport.test.ts` and `data/search-fields.ts:198`. Comments are
not this card's share, and none is touched here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants