Conversation
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…nner (#17556) Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…erate artifacts (#17556) Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…s surface (#17556) Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…is not a metadata collection (#17556) Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…e option-B pin (#17556) Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d6f451e9e8869d73996ffe2e49f6ffd79d83022 && git checkout 3d6f451e9e8869d73996ffe2e49f6ffd79d83022
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee5812a5e3931b64037cb46e255649a9da0e7b74 53f78a5796d40518d7864da14dee122d52d0bfc8 && git checkout -B drift-repro ee5812a5e3931b64037cb46e255649a9da0e7b74 && git merge --no-ff 53f78a5796d40518d7864da14dee122d52d0bfc8
node scripts/docs-audit/affected-docs.mjs --json ee5812a5e3931b64037cb46e255649a9da0e7b74
|
Seat note — carrier completed, two readings a reviewer will need
1. The contract-review carrier is now whole. 2.
So no member is added and none removed anywhere in 3. The body's coverage line understates itself, and the seat has NOT re-measured it. The Verification section records 2 of 131 derived gate families as NOT MEASURED — Generated by Claude Code |
Fixes #17556
Clause-②: yes (widening)
Suggestion 1 of #17081, filed onto its own card so PR #17555's
Closeswould not take itdown with the parent. #17555 took the platform's half — the
🔑 Dev adminline now sayswhat that account will and will not see. This is the half
packages/clistructurallycould not write: the platform can describe the account it seeds, and it cannot know that an
application's
Hiringgroup needs a position or which of five personas a demo should openwith. The application knows, and now has a channel to say so.
What an operator sees
Nothing changes unless the application declares something, and nothing changes outside a
development boot. With both keys declared:
The accept-set delta, stated so a contract review can check it
The top-level stack door has been strict since #8687, so both spellings were an
unrecognized_keysrefusal before this change. What is writable after that was notwritable before, and nothing else:
ObjectStackDefinitionSchema.devHintunrecognized_keys, path[])string, optionalObjectStackDefinitionSchema.devLoginsunrecognized_keys, path[])DevLogin, optionalDevLogin.emailstring, required, email-validatedDevLogin.passwordstring, optionalDevLogin.labelstring, optionaldevLoginsentrystrictObjectfrom birthNothing is renamed, nothing is retired, no value that parsed before is refused now, and
the delta is recorded in two tracked artifacts a review can read rather than take on
trust:
packages/spec/authorable-surface/system.jsongains exactlysystem/DevLogin:email,:label,:password, andpackages/spec/api-surface-declarations/root.txtgains the two keys onObjectStackDefinitionSchema.New exports, from
@objectstack/spec/system:DevLoginSchema,DevLogin,DevLoginParsed.Where the declaration belongs, measured
The card said its landing point was measured as
packages/specyet named only twopackages/clifiles. Both readings are right and they are two different halves:packages/spec. The banner prints once per boot, for the whole bootedstack, so the key is stack-level — not per-
App(N apps, one banner) and not per-package(the reader looks at the top level).
devPluginsis the standing precedent for adevelopment-only, CLI-consumed, stack-level key.
packages/cli.serve.tsalready holds the parsed definition in thesame scope as the banner call (it reads
config.flowstwo lines away, andconfig.devPluginsin the same function), so the values reachprintServerReadywithno new plumbing.
DevLoginSchemaitself lives inpackages/spec/src/system/dev-login.zod.ts, mounted intostack.zod.ts— the same shapeStackServerConfigSchemaalready has. Two reasons, onearchitectural and one mechanical: element schemas live in their domain file rather than in
the 4000-line envelope, and declaring it inside
stack.zod.tsdisplacedObjectStackDefinitionSchemaoff the five-export line ofskills/objectstack-platform/references/_index.md, dragging a PD-14 governed surface intothis diff for a generated one-line churn. Measured both ways: with the schema in
stack.zod.ts,check:skill-refswent red and regenerating produced that churn; with itin
system/,check:skill-refsis green and the file is untouched. This PR touches nogoverned surface (
docs/adr,.claude,skills,AGENTS.md,CLAUDE.md— thedocs/path here isdocs/audits/, a regenerated ratchet count).Suggestion 1 was not partly landed by #17555 — read first-hand
PR #17555's file list is three files: its changeset,
packages/cli/src/utils/format.ts, and one new test. Nopackages/specfile. Its bodysays so in its own words: the card's suggestion 1 "is the right long-term shape and is
not taken here: it needs a new key on a published schema in
packages/spec, anotherseat's lane". Independently re-derived on
origin/mainatb7eaf6a61:git grep -nIfordevLogins|devHintprinted no lines at all; the lit control (seededAdmin, the samesubject) printed 3 files; the dark control (an impossible identifier) printed none. A second
probe over
packages/spec/srcfor any pre-existing dev-credential declaration(
devAdmin|devLogin|devCredential|firstRun|devPersona|seedAdmin) printed exactly one line,an unrelated
"u_devadmin"group key inside an approval-service docblock; lit control(
dev-prefixed keys) printed 4 lines, dark control none.Three decisions the card left to the taker
The card listed three, marked "⛔ not prescribed here". Taken, with reasons:
Where the key lives, and one hint or a list — both, and stack-level. They answer
different needs and each has its own composition rule:
devLoginsis'concat', socomposing two applications keeps both publishers' personas (dropping one would hide an
audience the composed artifact still serves — the
packagesargument, one layer over);devHintis'single', so two stacks with contradicting first-run instructions is acomposition error naming the key, never a silent last-wins. Both are artifact ENVELOPE
keys: refused inside
packages[].manifest, exactly asdevPluginsis, because thebanner's only reader looks at the top level and a folded copy would be parsed, stored
and never printed.
Is printing application-supplied credentials safe — yes, with two constructions.
The content is not an escalation: anyone who can write
devLoginscan already writeonEnable, a function the dev process executes, so these values carry the trust levelof the code being booted. Two things genuinely are new, and both are closed by
construction rather than by assurance:
escape sequence in a hint could erase the rows above it or repaint a forged
🔑 Dev adminrow — the platform's own banner lying on the application's behalf.Every C0/C1 byte is replaced with U+FFFD before printing, so an entry occupies
exactly the rows it was given. Visible characters pass through unchanged: this is
neutralisation, not redaction, so an author who wrote one can see that they did.
isDev(
--devorNODE_ENV=development— the same condition family that letsmaybeSeedDevAdminfire at all). Any other boot renders byte-identically to onedeclaring nothing, so ADR-0115's attention-budget paragraph is untouched on the path
it was written about.
The residual, stated rather than hidden: these values are committed to the
application's repository already. The schema says so where an author reads it —
development fixture, never a real secret, never a value that also opens a deployed
environment.
What the banner does when an application declares nothing — exactly what it does
today. Byte-identical, asserted for absent, empty-array and blank-string
declarations, with a positive control proving the same comparison does move when
something is declared.
One decision the card did not list, taken the same way: the block is additive, beneath
the seeded-admin lines and never instead of them. An application-controlled key able to
suppress a platform disclosure would let an app hide a live credential the operator was
just handed. And declaring is not seeding: an entry creates no account, which is why
the banner says the application declared it — an entry naming an unseeded account then
reads as the app's claim rather than as a broken platform.
Red / green / dark
New pins:
packages/spec/src/stack-dev-logins.test.ts(13) andpackages/cli/src/utils/format.server-ready-app-logins.test.ts(13). Rejections assert theZod issue
codeandpath, never a bare throw.safeParseand throughdefineStack(the An unknown top-level stack key is named but still not rejected — and the diagnostic is not a warning, soobjectstack validate --strictcannot catch it either (17.0.0 GA) #8687 defect was a key that parsed green and was then stripped);devLoginsconcatenates across a compose; an identicaldevHintpasses through.unrecognized_keysat path['devLogins', '0'];usernameandnoteare routed toemailandlabelby thecurated aliases; a missing address raises
invalid_typeatdevLogins.0.email; anon-address is refused at the same path; a non-string
devHintraisesinvalid_type;two differing hints throw naming
top-level key 'devHint'.devLoginz,devHintsanddevCredentialsstill raiseunrecognized_keysat path
[], so the green above reads as "two keys were declared" and not as "the strictclose regressed". On the banner side, control-byte-free text passes through with no
U+FFFD anywhere, so "the bytes were replaced" is a measurement.
Ablations (
scripts/ablation-replace.mjs, on-disk proof and restore verified by blobhash plus an empty
git diff HEADeach time):.email()fromDevLogin.emaild6b5d27f8e38tob0c0abd3c80cdevLoginskey from the stack shape11f3c284217atob2611a37d33fCross-package reverse verification —
packages/clitypes against a schema built inpackages/spec, so the green had to be shown to be a reading of the rebuilt.d.tsandnot a cache. Replacing
entry.emailwithentry.nopeinformat.ts(anchor 1 to 0, blob8143c65513c9to1375d1655de6) turnedpnpm --filter @objectstack/cli typecheckredwith
TS2339: Property 'nope' does not exist on type '{ email: string; password?: string | undefined; label?: string | undefined; }'— the message quotes the exact shape declared insystem/dev-login.zod.ts. Restored, blob back to8143c65513c9,git diff HEADempty,typecheck green again.
Verification
Measured at
53f78a5796, this branch.pnpm --filter @objectstack/spec test— 495 files / 14534 tests pass;typecheckgreen.pnpm --filter @objectstack/cli exec vitest run --project unit— 218 files / 3077 tests pass;typecheckgreen. Theintegrationtier is declared to CI (pnpm testruns both); the diff adds no spawn entry point and changes no boot behaviour beyond two option values.pnpm --filter @objectstack/spec check:generated— 16 of 16 artifacts up to date.pnpm lint(eslint . --no-inline-config, the whole repository, not a narrowed run) — green in 78s.node scripts/pm/dispatch-gates.mjs --ran— 131 derived families accounted for: 129 run green, 2 NOT MEASURED. The two arecheck:dual-build-cjs-loadsandcheck:type-check-debt, which both exited 3 — PREREQUISITE NOT MET: each needs every workspace package built, which is a repository-wide build CI performs on a fresh checkout. Neither is a failing measurement.check:type-check-coverage, the per-package half of the same pair, is green.Two gates asked for a hand edit and got one rather than a regeneration, because neither has
a generator:
packages/spec/llms.txt(the system domain is 34 schemas, the tree 202) andcontent/docs/getting-started/quick-reference.mdx(System Protocol is 16 of 34; the tableis a curated subset, so N is left alone — adding a row is a decision, not a fix).
Acceptance notes
Found while measuring, deliberately not fixed here:
packages/spec/src/stack.zod.ts, theSTACK_DEFINITION_COLLECTIONS_SHAPEdocblock saysthe envelope keys "stay on the schema below and are deliberately NOT in here:
manifest,packages,api,server,i18n,runtimeModuleandonEnable". Fiveof those seven are in fact declared inside that shape (
api,server,i18n,onEnable,runtimeModule); onlymanifestandpackagesare outside it. Commentonly — no runtime or accept-set effect. The paragraph this PR does edit is the separate
one about
plugins/devPlugins, which was accurate and is kept accurate; the stalesentence is left untouched rather than corrected as a rider.
packages/cli/src/commands/serve.ts, inresolveStorageCapabilityArg's header: thereason
config.storageis not read says "the schema is not.strict(), sodefineStack... strips it beforeservecould ever see it". An unknown top-level stack key is named but still not rejected — and the diagnostic is not a warning, soobjectstack validate --strictcannot catch it either (17.0.0 GA) #8687 closed that door —the top level is
strictObjectand an undeclaredstorageis now refused at parse, notstripped. The conclusion still holds; the stated mechanism is one release stale. Comment
only.
Neither is a reproducible defect, a contract violation, or a trap that makes an author
write metadata the runtime rejects or drops, so neither is filed as a card.
What this PR does not do
stays exactly as it was; only what the banner can be told to print moves.
objectui.packages/spec/src/ui/app.zod.ts: anApp-level field was consideredand rejected on the measurement above (one banner per boot, N apps per stack), and that
file is in flight on another card.
Generated by Claude Code