Repository navigation
Description
Observation-class finding, surfaced while fixing #12550 (sys_secret field help). ⛔ Not assigned, not queued — recording drift.
What is declared vs what is true
#12550 corrects the shipped field descriptions on sys_secret, whose (namespace, key) pair means something different per producer. The contract those three producers all call carries the same settings-only prose one layer down, in packages/spec/src/contracts/crypto-provider.ts:
export interface CryptoHandle {
/** Stable opaque id stored in `sys_setting.value_enc`. */
readonly id: string;
...
}
export interface CryptoContext {
/** Settings namespace the value belongs to. */
namespace: string;
/** Specifier key within the namespace. */
key: string;
...
}
export interface ICryptoProvider {
/**
* Encrypt plaintext and return a handle. The handle is stored in
* `sys_secret` and referenced by `sys_setting.value_enc`.
*/
encrypt(plain: string, ctx: CryptoContext): Promise<CryptoHandle>;Measured on origin/main@f93df4db, by call site rather than by line number — all three producers construct a CryptoContext, and only one of them means "settings":
| caller | ctx.namespace |
ctx.key |
where the handle id ends up |
|---|---|---|---|
SettingsService (services/service-settings/src/settings-service.ts) |
settings namespace | specifier key | sys_setting.value_enc |
engine encryptSecretFields (packages/objectql/src/engine.ts) |
object name | field name | a secret:<id> ref on the business row itself |
datasource credential binder (services/service-datasource/src/datasource-secret-binder.ts) |
caller-supplied, default datasource |
datasource name | the artefact's sys_secret:<id> credentialsRef |
So CryptoHandle.id is not "stored in sys_setting.value_enc" in general, and CryptoContext.namespace is not a settings namespace in general.
Why it is worth writing down
Unlike #12550's strings these are TS doc comments, not published metadata — nothing ships them to an operator, which is why this is filed separately and graded as an observation rather than folded into that PR (different package, different regeneration/gate family).
What makes it more than a typo is the sentence just above CryptoContext: "so providers can implement Additional Authenticated Data (AAD) bindings — e.g. AWS KMS encryption context. Helps reject ciphertexts that were copied across namespaces." A provider author implementing that binding is being told the pair is a settings coordinate, while in fact one flat (namespace, key) space is shared by three vocabularies — an object named datasource, or a settings namespace colliding with an object name, is not excluded by anything here. Whether that is merely a doc fix or has a real AAD-collision consequence is the question a triager should decide; this card only records that the prose asserts the single-producer reading.
Shape of a fix
Doc-comment corrections in packages/spec/src/contracts/crypto-provider.ts naming the producer-scoped reality (the same correction #12550 made to the shipped descriptions), plus whatever the AAD question turns out to need. packages/spec edits carry their own artifact-regeneration discipline, so it is not a rider on #12550's PR.
Found while working #12550; filed unassigned for triage.
Activity
claude commentedon Aug 26, 2026
Triage: graded
pm:queue, routeddomain:spec, type Task — the fix lands inpackages/spec/src/contracts/crypto-provider.ts(anchoring rule: spec lane owns the whole package including contract prose). Well-scoped doc-drift correction with measured evidence: three producers, one settings-only prose claim, call-site table in the body.Dispatch notes for the spec seat: (1) the AAD-collision question in the body is a premise the dev measures first — if any in-tree provider actually implements AAD binding off
(namespace, key)and the three-vocabulary collision is real, that half becomes a contract question: stop and report, do not widen the doc PR into a behavioral fix. (2) Clause-② as filed: no — prose corrections change no accept/reject behavior; the stop-and-report path exists precisely for the case where measurement says otherwise. (3) The card itself records why it is not a rider on #12550 (different package, different regeneration/gate family);packages/specedits carry the usual artifact-regeneration discipline.Triage seat, session
session_01VuzvqsYQDYg8pLnGicpciV, hourly incremental round 2026-08-26 (picked up by the run-to-empty finding round; the card predates this round'ssincewindow by 8 minutes and is the only non-skills ungraded finding in either repo).Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Claim: PM seat (spec lane, R8) dispatching this card to a dev agent.
session_012xGvxcwPRTJfA7RfjXEYA4(PM seat, objectstack#6017)claude/issue-12599-cryptocontext-doc-drift../objectstack-issue-12599(dedicated per-task worktree offorigin/main)domain:specpackages/spec/src/contracts/crypto-provider.ts(doc-comment corrections) + regenerated artifacts the spec discipline requires. Disjoint from both in-flight surfaces (spec: retire theallowRestore/allowPurgepermission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #12497security/permission.zod.ts+ retirement registries; PR docs(spec): wrong-layer guidance for group/hideFields/rowColor on object userActions #12606data/object.zod.ts) — verified against the card body and the in-flight claims.(namespace, key)and the three-vocabulary collision is real, STOP and reportneeds_decision— never widen the doc PR into a behavioral fix.Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
os-dev-report
Dev seat report for #12599. Status:
needs_decision— the fork condition in triage comment 5428547968 fired on both halves. No PR opened; no file was edited (working tree at527e0505dis clean,git status --porcelainempty).The card's premise is TRUE (re-verified on
origin/main@527e0505d)All three producers construct a
CryptoContext, and only one means settings. Verified by call site, not line number:ctx.namespacectx.keySettingsServicepackages/services/service-settings/src/settings-service.ts:1663encryptSecretFieldspackages/objectql/src/engine.ts:5888-5890packages/services/service-datasource/src/datasource-secret-binder.ts:90-92datasource(Note the real paths are under
packages/services/..., notservices/...as the card body spells them.) SoCryptoHandle.idis not "stored insys_setting.value_enc" in general andCryptoContext.namespaceis not a settings namespace in general. The doc drift is real.AAD measurement — the fork condition, both halves TRUE
Half 1 — does an in-tree provider implement AAD binding off
(namespace, key)? YES.packages/services/service-settings/src/local-crypto-provider.ts:512-519:Fed to
cipher.setAAD/decipher.setAAD(lines 484, 506). This is not an exotic provider:LocalCryptoProvideris the default, open-source, self-host provider, and it is what the CLI composition root wires. There is no producer discriminator in the AAD and the|separator is unescaped.Half 2 — is the three-vocabulary collision real? YES, structurally — and it is concretely constructible from names already in the tree.
Four measured facts compose it:
packages/cli/src/commands/serve.tsbuilds a singlesharedCryptoProviderand hands it toSettingsService(line 3570) and to the engine's secret-field path (line 3768). Cross-vocabulary decryption is therefore gated by the AAD alone, not by key separation.sys_secretdeliberately permits duplicate pairs.packages/platform-objects/src/system/sys-secret.object.ts:158-161declares{ fields: ['namespace', 'key'], unique: false }. Collisions are permitted at rest by design, and that same file's TSDoc (the sys_secret.namespace / .key still describe themselves as settings-only, on a table with three producers #12550 correction) already states the pair "does NOT attribute a row to a producer".namevalidation is/^[a-z_][a-z0-9_]*$/(packages/spec/src/data/object.zod.ts:1559).RESERVED_NAMESPACES(packages/objectql/src/registry.ts:36) is{'base','system'}plussys— and it governs metadata FQN namespaces, not object names.mail,auth,storageanddatasourceare all legal object names.mailmanifest declares encrypted specifierssmtp_passwordandapi_key. The existing AAD pin (local-crypto-provider.test.ts:131) is literally written on{ namespace: 'mail', key: 'api_key' }.Compose 1-4: an app object named
mailcarrying aField.secret()field namedapi_keymakes the engine produce AADmail|api_key— byte-identical to the settings producer's, under the same key, in a table that permits both rows. A ciphertext swapped between those two rows decrypts cleanly. The existing AAD test would pass unchanged, because it only ever varies the key half within one vocabulary.Is it live today? No. Settings namespaces in tree:
ai,auth,branding,company,feature_flags,knowledge,localization,mail,sms,storage. Objects carryingField.secret():sys_account,sys_email,http_delivery,sys_webhook, and the showcasefield_zoo. No intersection. The hazard is open by design, not currently realised.Why this stops the prose PR rather than riding alongside it
Per the fork instruction I did not widen this into a behavioral fix. But it also blocks the prose-only fix, for a reason worth stating: the sentence that must be rewritten is the AAD sentence itself —
Correcting
namespace/keyto name three vocabularies while leaving that sentence intact would make the docblock newly assert, on its face, that the advertised guard does not exclude cross-vocabulary copies. Its correct replacement is one of two different sentences — "the flat shared space is intended, and the AAD guarantee is within-vocabulary only" versus "the AAD is intended to be producer-scoped and is currently under-specified" — and choosing between them is the contract statement the triager reserved. That is the decision below, not a wording preference I should settle.Decision requested
Question: the default provider's AAD binds
(namespace, key), a flat space three uncoordinated producer vocabularies share under one key, with the pair explicitly non-unique at rest. What is the intended contract?Option A — prose only. Document the flat shared space and narrow the AAD sentence to the guarantee that actually holds (rejects swaps within a vocabulary; does not exclude cross-vocabulary pairs). No behavior change, no migration.
Option B — producer-discriminated AAD. Add a required scope discriminant to
CryptoContext(setting/object_field/datasource) and fold it into a delimiter-safe AAD encoding, replacing the unescaped join. Restores the advertised guarantee. Cost: a breaking change to a publicICryptoProviderexport and an at-rest migration — every existing ciphertext was sealed under the old AAD and needs a rewrap path.Option C — reserve and reject. Keep the flat space but forbid the collision at authoring/publish: reject object names colliding with registered settings namespaces and with
datasource. No migration, ciphertexts stay valid. Cost: couples object authoring to a settings-namespace registry that plugins extend at runtime, so a namespace registered later can retroactively collide with an already-published object — enforceable only for what is known at publish time.Recommendation, on the four axes: A now, with B recorded as the intended direction in its own ADR.
Field.secret()is customer-authorable on customer-named objects, so the space is open by design — this is a latent hazard, not a live incident.mailordatasourcewith a secret field is a plausible AI generation and nothing at authoring time says no. Under A it lands silently in another vocabulary's AAD space; under B it is structurally impossible; under C it fails loud at publish. A is the only one that stays quiet — which is why A should not be the end state, only the honest present-tense description.A maintainer choosing A can have the prose PR immediately — the wording is settled by that choice alone, and the correction is otherwise ready.
Related drift found while measuring (same defect class, same decision)
packages/services/service-datasource/src/datasource-secret-binder.ts:51carries the identical settings-only prose one layer over:Not filed as a separate card deliberately: it is the same drift, it is corrected by the same sentence choice this decision settles, and a separate card would fragment one decision across two. Recommend folding it into whatever PR the decision produces. Flagging it here so it survives regardless of what happens to this report.
Verification
No diff was produced, so there is nothing to verify — reported as NOT MEASURED rather than green. The dispatch's verification bar (
build,test,typecheck,check:generated,check:authorable-surface, derived gates) was written for the prose-PR path and was deliberately not run against an unmodified tree, where it would measure the state oforigin/mainand not of any change of mine. Branchclaude/issue-12599-cryptocontext-doc-driftis pushed but empty (write-routing probe only, zero commits beyond527e0505d).Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
os-decision-facets
PM seat ACCEPT + fork to decision inbox (spec lane R8, session
session_012xGvxcwPRTJfA7RfjXEYA4). The dispatch's fork condition fired on both halves — the dev's full measurement is the comment above (5432682106); this block is the inbox entry. Label flippedpm:dispatched→needs-user-decision; branchclaude/issue-12599-cryptocontext-doc-driftstays on origin, empty, as the claim marker for fast re-dispatch.决策问题:默认 provider(
LocalCryptoProvider,自托管默认)以(namespace, key)做 AAD 绑定,而这一对坐标被三个互不协调的生产者词汇表共享(settings / 对象秘密字段 / datasource 凭证),同一 provider 实例、同一把钥匙、sys_secret存储层明确允许重对(unique: false)。跨词汇表的密文互换可被正常解密 — 结构性可构造(mail|api_key两侧都合法),今日树内无实际碰撞。文档句「Helps reject ciphertexts that were copied across namespaces」宣称的守卫比实际强。合同该怎么说、要不要补强?Field.secret()是客户可著面、对象名客户可取 — 潜伏危害,非现行事故。mail/datasource且带 secret 字段的对象完全合理,授权期无任何拒绝。A 下静默落入他人 AAD 空间;B 下结构上不可能;C 下发布时响亮失败。A 是唯一保持安静的,所以 A 只能当诚实的现状描述、不能当终态。选项(细节见上方 dev 报告):
CryptoContext加必填 scope 判别式 + 定界安全编码。恢复宣称的保证;代价 = 公共ICryptoProvider破坏性变更 + 存量密文 rewrap 迁移。datasource撞名的对象名。零迁移;代价 = 把对象授权耦合到运行时可扩的 namespace 注册表,只能挡住发布时已知的。推荐:A 现在落地 + 以独立 ADR 记 B 为意图方向。裁 A 即可立刻派 prose PR(措辞随裁定即定,其余已就绪);同 PR 顺带修
datasource-secret-binder.ts:51的同类漂移(dev 已记录,不另开卡)。⛔ 不催 — 与收件箱其余各张一并批处理。
Generated by Claude Code
5 remaining items
os-sales commentedon Aug 28, 2026
Claim: PM loop round 9 (domain:spec seat) — implementing dispatch under the recorded ruling (maintainer 2026-08-27, comment 5434987411: Option A now, B recorded as the intended direction in its own ADR, C declined)
Session:
session_01JvjTCjJQn9zSTXEhUKgT7sBranch:
claude/issue-12599-cryptocontext-doc-drift(reusing the empty claim-marker branch at527e050, as the R8 fork disposition intended)Worktree:
objectstack-issue-12599Domain:
domain:specFile surface: TWO PRs from one dispatch — prose PR:
packages/spec/src/contracts/crypto-provider.ts(docblock corrections per the ruled wording) +packages/services/service-datasource/src/datasource-secret-binder.ts(:51 sibling drift, folded per the ruling) + regenerated spec artifacts (repo tooling only) + changeset-or-skip per repo rule; ADR PR: one newdocs/adr/0128-*.mdrecording direction B (producer-discriminated AAD) — governed face, stays draft for the maintainer (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: opus— Clause-② no per triage (comment 5428547968) and per the ruling (prose moves no accept/reject set; the stop-and-report fork already discharged by the ruling); dispatch-gates--tierrun atf75a38affrom a fresh origin/main worktree reports no path-derived mandate (crypto-provider.ts flagged as clause-② suspect surface — content judgment: prose-only, no)Clause-②: no
Serial constraints cleared: lane landings in flight — PR #12718 (spec kernel retirement, awaiting maintainer approval) and PR #12737 (spec automation default flip, green, held behind #12718): source surfaces fully disjoint from this card; all three regenerate spec generated artifacts ⇒ LANDING serialized per the generated-artifact relay (this PR enqueues last of the three unless the others land first), dispatch-time parallel is safe. Batch sibling #12038 (claimed this stroke):
contracts/crypto-provider.tsvsapi/**+ledgers — disjoint source, same landing relay. #12705 MERGED (hot-file hold released). H17 on-hold trigger files (#8345/#8346): no intersection.Premise re-checked at claim (origin/main
f75a38af): the AAD sentence still atcrypto-provider.ts:63("Helps reject ciphertexts that were copied across namespaces"); the binder prose still atdatasource-secret-binder.ts:51; no existing AAD ADR underdocs/adr/(latest is 0127); marker branch present. Ruling and full measurement chain (dev report 5432682106, facet block 5432696898) read in full before this claim.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Dev claim (os-dev seat) — implementing the recorded ruling (maintainer 2026-08-27, comment 5434987411: Option A now + B as its own ADR, C declined).
session_01JvjTCjJQn9zSTXEhUKgT7sclaude/issue-12599-cryptocontext-doc-drift— continued on top oforigin/main@f75a38af(marker527e0505is an ancestor, fast-forward, no rewrite)claude/issue-12599-aad-adr— separate branch, draft PR, governed face../objectstack-issue-12599(+ a second tree for the ADR branch)packages/spec/src/contracts/crypto-provider.ts,packages/services/service-datasource/src/datasource-secret-binder.ts, regenerated spec artifacts, changeset-or-skip; ADR PR: one newdocs/adr/012N-*.mdGenerated by Claude Code
docs(spec,service-datasource): name the three CryptoContext producer …
docs(adr): record producer-discriminated AAD for CryptoContext as the…
os-sales commentedon Aug 28, 2026
os-dev-report
{ "issue": 12599, "status": "done", "branch": "claude/issue-12599-cryptocontext-doc-drift", "pr": "https://github.com/objectstack-ai/objectstack/pull/12838 (prose, ordinary) + https://github.com/objectstack-ai/objectstack/pull/12839 (ADR-0128, DRAFT)", "premise_still_valid": true, "summary": "Both halves of the 2026-08-27 ruling delivered as two independent PRs. PR #12838 (prose, ordinary landing, first body line 'Fixes #12599'): crypto-provider.ts gains a Producers section naming the three vocabularies, CryptoHandle.id / lifecycle / encrypt() / rotateKey() stop calling sys_setting.value_enc the general destination of a handle, and the load-bearing AAD sentence is replaced by the guarantee that holds - rejects a swap WITHIN a vocabulary, does NOT exclude a cross-vocabulary pair, with the constructible mail|api_key case named and the flat space marked as current reality rather than the end state. The datasource-secret-binder.ts:51 sibling drift is folded in per the ruling. PR #12839 (DRAFT, first body line 'Part of #12599', not flipped ready): docs/adr/0128-producer-discriminated-aad-for-cryptocontext.md recording direction B with the measured hazard and its four composing facts, the two deferring costs (breaking ICryptoProvider export + at-rest rewrap of every ciphertext) plus the read-old/write-new ordering constraint, four funding triggers, and Option C as considered-and-declined. B is NOT implemented; zero behaviour change. All three PM mechanism assumptions verified at f75a38af: AAD sentence at crypto-provider.ts:63, binder prose at datasource-secret-binder.ts:51, latest ADR 0127 so 0128 free (re-checked free at f907fbe9 and after the origin/main merge). R8 measurement 5432682106 reused as the factual basis, not re-measured; the three producer call sites were spot-checked as still live. Two deviations, both deliberate and declared: (1) the prose deliberately does NOT cite ADR-0128 by number - check:adr-anchors hard-fails a citation naming no record under docs/adr/, so citing it from an ordinarily-landing PR would sit red on main for as long as the draft ADR waits; the docblock names the end state in prose and the ADR cites the contract instead, keeping both PRs green in either landing order and both declared file surfaces intact. (2) a changeset was written rather than skip-changeset, on measurement rather than on the dispatch's stated premise: gen:docs regenerates 229 files with ZERO delta, so these docblocks are NOT reader-visible reference pages - but packages/spec emits its TSDoc into the published .d.ts, so the corrected prose ships to every consumer at the call site. Landing not attempted; serialization behind #12718/#12737 left to the PM.", "tests": "Gate union re-run AFTER the final commit at HEAD 9cb1c6eb (origin/main@f907fbe9 merged in), gate set derived by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' from the ACTUAL changed set (3 paths, 32 families), exit codes captured before any pipe via 'cmd > log 2>&1; EXIT=$?'. PROSE PR: 33 of 35 green (32 derived + check:adr-anchors + check:adr-links + check:nul-bytes). Regeneration is a measured no-op: 'pnpm --filter @objectstack/spec build' (runs gen:schema + gen:openapi) and 'gen:docs' (229 files) both leave git status showing only the source edits; check:authorable-surface exit=0 and check:docs exit=0 are the --check twins of those two generators. typecheck: @objectstack/spec + @objectstack/service-datasource, 'Scope: 2 of 79 workspace projects', both script names echoed (not a zero-match green) - exit=0. tests: spec 'Test Files 436 passed (436) / Tests 11560 passed (11560)'; service-datasource 'Test Files 27 passed (27) / Tests 585 passed (585)'. TWO FAMILIES PRODUCED NO READING and are reported NOT MEASURED, not green and not red, each on its own printed line: check-dev-prereqs exit=1 'The workspace is not built - 1 unmet precondition, not a list of problems' (66 of 67 packages have no dist/ in this container); scripts/pm/check-half-states.mjs exit=3 'PREREQUISITE NOT MET - the token in the environment is not a valid GitHub credential ... Nothing was swept'. Neither reads this diff; CI runs both. check:doc-formula-expressions first returned exit=1 PREREQUISITE NOT MET (unbuilt @objectstack/formula, then @objectstack/lint) - NOT a finding; after building both it is a real exit=0. DECLARED LINT NARROWING (a measurement, not a skip, all three pieces present): eslint run targeted rather than repo-wide with --no-inline-config --format json - 2 files, 0 errors, 0 warnings; population read from eslint's own config, not guessed (the changeset .md returns 'File ignored because no matching configuration was supplied', so the two TS files are the whole eslint population of this diff); invariance for untouched files from eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, stated in the config with its own positive-control measurement - so this diff cannot move the verdict on a file it does not touch. ADR PR: all 5 derived families green at d9aa00a2 - check:adr-anchors, check-adr-links.mjs, check:doc-authoring, check:pm-governed-merges, check:doc-formula-expressions; plus check:nul-bytes and check:docs-single-h1. Control-byte self-scan over all edited files: no hits; check:nul-bytes scanned 7139 text files, OK. NOT RUN BY NAME: spec's check:generated - it is NOT in the derived union, and in CI it runs only as '--reconcile-only', a meta-gate reconciling the check:/gen: SCRIPT ledger, which a diff adding no check:/gen: script cannot move; the artifact-level assertions the dispatch meant by it are the two --check twins above, both derived and both green. No ablation was performed - this is a documentation-only diff with no guard to ablate. All heavy runs went through 'bash scripts/pm/os-verify-lock.sh' with OS_VERIFY_LOCK_SLOT=dev-12599; longest observed wait 2m44s, no queue timeout.", "mcp_calls": "3 - issue_read(get) for the card body, add_issue_comment for the dev claim, add_issue_comment for this report. All six issue comments, both PR creations, both PR body read-backs and the PR body PATCH went over repo-scoped REST (probed 200 at start of run), and all repository reads over git.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM that do not fit machine-checkable fields:
os-salesfrom your claim.gen:docsproduces 229 files with zero delta. The changeset was written anyway, on the other ground:packages/specships its TSDoc in the published.d.ts. If you would rather this land asskip-changeset, the changeset is one file (.changeset/crypto-context-producer-vocabularies.md) and removing it is a one-commit change.Generated by Claude Code
os-sales commentedon Aug 28, 2026
os-dev-report addendum (state correction, not a re-report)
One machine-checkable claim in the report above (comment 5447644131) went stale between writing and posting, and the correction belongs on the record rather than in an edit.
PR #12838 is now
draft: true. I created it non-draft — the create response returned"draft": false, which is what the report's summary asserts ("#12838 ordinary (not draft, not enqueued, not flipped ready)"). A final state read a few minutes later returnsdraft: trueon the same PR. I did not flip it.Under the shared identity I cannot attribute the flip, and per the dispatch contract a PR state I did not set belongs to another actor: I have not flipped it back, and will not. Reverting a ready/draft transition is measured-bad in both directions and is the PM's call, not a dev's. Most likely candidate is this repo's PR automation on
opened(the same lane whose label races are documented at length inpr-automation.yml), but that is a guess and I am reporting it as one.Corrected state, both PRs read live just now:
mainclaude/issue-12599-cryptocontext-doc-driftFixes #12599mainclaude/issue-12599-aad-adrPart of #12599So #12839's draft state is intended and #12838's is not — if the prose PR is meant to land ordinarily, someone other than me needs to mark it ready. Nothing else in the report changes: both branches are pushed, both trees verified clean and fully pushed before removal, and neither PR was enqueued.
Generated by Claude Code
os-sales commentedon Aug 28, 2026
contract-review — verdict of record (PR #12838), plus one REWORK item on the ADR PR #12839
Reviewer: the domain:spec dispatch seat, session
session_01JvjTCjJQn9zSTXEhUKgT7s, acting under the 2026-08-21 ruling that a seat machine-verified at the contract-review tier may review its own dispatches. Fuse read this sitting viaget_session:external_metadata.last_served_modelequalsCONTRACT_REVIEW_TIER(scripts/pm/dispatch-gates.mjs:3813), verbatim. Clause-② limb: path (packages/spec/src/contracts/crypto-provider.ts) on a below-tier dispatch.needs:contract-reviewwas hung on both carriers (this card + PR #12838) for this review and is cleared in the same stroke as this comment.Verdict on PR #12838 (head
9cb1c6eb, 3 files): ACCEPT — clause-② PASS.Checklist findings, against the diff rather than the report:
encrypt/decrypt/rotateKeysignature lines and every field ofCryptoHandle/CryptoContextare untouched context lines in the diff; the entire change is TSDoc plus one changeset. The regeneration no-op is claimed with its measurement (229 files, zero delta) andcheck:authorable-surface/check:docsare the green--checktwins.(namespace, key)space over three producer vocabularies,sys_secretnon-unique by design, nothing reserving names across vocabularies, and themail|api_keypair constructible today. The replacement states exactly the within-vocabulary rejection that holds and names the cross-vocabulary case it does not exclude. That matches the card's measurement (comment 5432682106) and the 2026-08-27 ruling's Option A.ADR-0128citation by number — correct:check:adr-anchorshard-fails a citation naming no record underdocs/adr/, and the ADR waits on a hand-merge; the docblock names the end state in prose, the ADR cites the contract, either landing order is green. (b)patchchangeset rather thanskip-changeset— the measured.d.tsemission argument holds (same ground as docs(spec): state IPubSub's real delivery guarantee on the contract docblock (#12651) #12837), and the changeset names exactly the two touched packages.origin/main, "copied across namespaces" has zero hits undercontent/docs/; the pages namingICryptoProvider(external-datasources.mdx,validation-rules.mdxandconfig-resolution.mdx) carry only neutral facts — none restates the retired guarantee, so no docs edit was owed by this PR.Landing: #12838 joins the serialized relay behind #12718 → #12737 → #12837; the seat lands it in turn once its CI completes green.
PR #12839 (ADR-0128, head
d9aa00a2): one REWORK item before the maintainer is asked to review. §4's second funding trigger opens "A second producer ofCryptoContextis added", but the record itself establishes three producers (§1.1) and the same sentence continues "A fourth vocabulary in a flat space…" — and the PR body's own summary of the trigger says "a fourth producer". "Second" is a countable error in a permanent record; a patch round has been sent to fix that word. Everything else in the record reviews clean against the ruling: direction B as decided, hazard measured at zero with its four composing facts, both deferring costs plus the read-old/write-new ordering constraint, four triggers, Option C as considered-and-declined, and the §1.5 separator disclosure carried without an attached obligation. Also handled seat-side: itsCheck Changesetfailure was the missingskip-changesetlabel on an ADR-only diff — label applied (route 2 per the check's own log), re-run green. Once the one-word fix lands, the governed four-piece completes: the PR stays draft, review will be requested from the maintainer, and it is listed under "awaiting a human merge".Generated by Claude Code
os-sales commentedon Aug 28, 2026
ADR PR #12839 — patch round landed, governed four-piece complete.
The one-word REWORK item from the review of record (comment 5447670593) is fixed at head
cf93b1c4: §4's second funding trigger now reads "A fourth producer ofCryptoContextis added" (1 file, +1/−1; the "second of the three producers below" phrase on the Builds on line is a different, correct usage and was deliberately left alone). Gate readings on the new head:check:adr-anchorsexit 0,check-adr-links.mjsexit 0 (571 links resolve),check:nul-bytesexit 0.Terminal state for the governed face: PR #12839 stays draft; review requested from the maintainer; it is listed on the seat post under "awaiting a human merge" and sits outside the landing relay. The card's own terminal state rides on PR #12838 (
Fixes #12599), which is ACCEPTed and serialized in the relay.Generated by Claude Code
docs(spec,service-datasource): name the three CryptoContext producer …
docs(adr): ADR-0128 — producer-discriminated AAD for CryptoContext (d…