docs(skills): objectstack-upgrade factual sweep — a phantom preflight key, an unsafe --from rule, a retired ETL target, and the withdrawn automatic-rewrite claim - #13861
Conversation
…cted Flight ⑨ of the published-skills factual sweep (#13658). Five behavioral claims verified FALSE against the implementation and corrected in place; net -3 lines, token-neutral at the ratchet ceiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
Contract review — PASS at head
|
Fixes #13840
Part of #13658
Flight ⑨ of the published-skills factual sweep. Target:
skills/objectstack-upgrade/**— one file,SKILL.md, 696 lines atorigin/main(PM premise re-derived from the fetched tree: holds, exactly one file, exactly 696 lines).Governed surface — this PR stays DRAFT and is merged by the maintainer, by hand. No ready-flip, no queue, no auto-merge (Prime Directive #14).
Result
~150 behavioral claims inventoried · 5 FALSE (distinct facts) · 5 landing sites · ~40 NOT MEASURABLE recorded. Density 3.3% / 3.3% — inside the 1.5–10% working range set after flight ⑧, near its floor.
skills/objectstack-upgradeis the healthiest surface swept since flight ②, which is itself the finding: the file's own design (a self-refreshing pinned-reading section, prescriptions quoted from the spec) is doing real work.Every correction is byte-neutral-or-shrinking and lands in the same passage as the fact it fixes. Ceiling untouched.
The five corrections — 落点 | before | after
1 · §0 preflight source (1) — a phantom key, taught as the most authoritative reading
1. **`manifest.protocol`** in the stack config (`'16.0.0'` → `--from 16`). This is the declared answer and the kernel checks it at load time.1. **`manifest.engines.protocol`** in the stack config (`'16.0.0'` → `--from 16`). The declared answer, checked by the boot handshake.manifest.protocolhad zero occurrences in the whole repo outside this one line (grep -rnover*.ts/*.tsx/*.md/*.mdx/*.json, node_modules excluded — one hit, the skill itself).ManifestSchema(packages/spec/src/kernel/manifest.zod.ts) declares noprotocolmember, and it is not.strict(), so an author who took the advice literally and wrote the key would have it silently accepted and dropped. Nothing "checks it at load time" because nothing reads it.The real key is
manifest.engines.protocol(PluginEnginesSchema, manifest.zod.ts:70), and it really is checked at load: the ADR-0087 D1 handshake refuses boot viaassertProtocolCompat(packages/runtime/src/app-plugin.ts) andassertArtifactProtocolCompatible(packages/runtime/src/artifact-reference.ts), raisingOS_PROTOCOL_INCOMPATIBLE. Flight ① called this flavour phantom fields with zero repo-wide hits.2 · §0 preflight source (3) — "the lower one is the safe
--from" is unsafe, and the skill's own §3.3 says soIf (1) and (2) disagree, the lower one is the safe --from — the chain is idempotent, so replaying a hop that has already been applied is a no-op, while skipping a hop loses its rewrites.If (1) and (2) disagree, settle it — ⛔ never default to the lower one: a default flip (3.3) stamps its constraint onto a source already past that major.Idempotence holds for renames, not for the default-flip class — and this file already documents that class in §3.3, about the one conversion where it matters.
field-required-notnull-explicit(packages/spec/src/conversions/registry.ts) fires on any field withrequired: trueand nostorageblock. A protocol-17-authored field is exactly that shape, deliberately: post-17requiredis the write contract only. The registry's own docblock states the hazard verbatim — "a loader that auto-applied this transform would stamp NOT NULL onto it, silently restoring the tri-binding the ADR removed".Executed (PROBE 4, below): a 17-canonical source parses green and an understated
--from 16replay stampsstorage: {notNull: true}onto it. Following the old advice adds NOT NULL DDL a customer deliberately did not ask for. Self-contradiction resolved in favour of §3.3.3 · §2.3 target table row 1 — an enumeration that stopped growing when the schema did
a flat string enum (`none`/`constant`/`map`/`split`/`join`/`lookup`) with settings in `params`, executed row by row by the REST import path.a string enum, settings in `params`; the REST import path runs `none`/`constant`/`map`/`split`/`join`, passes `lookup` to reference resolution, rejects `javascript` (400).The standing probe class from flight ⑧, hit on the first try.
TransformType(packages/spec/src/data/mapping.zod.ts) has seven members; the skill presented six. Executed (PROBE 6):["none","constant","lookup","split","join","javascript","map"].Both halves of the row were wrong, in opposite directions:
javascriptis a live schema member the doc omitted — and the REST import path rejects it with HTTP 400UNSUPPORTED_TRANSFORM(packages/rest/src/import-mapping.ts), the deliberate implement-or-reject-loudly posture. An agent that never learns the member exists cannot read that 400.lookupis not "executed row by row" — the module contract says the value is copied through, and the import pipeline's own reference resolution turns lookup names into ids, "so a dedicated re-implementation here would be a second dialect".The spec's own tombstone prescription (
packages/spec/src/shared/mapping.zod.ts) states both facts correctly, including the 400. The skill had quoted half of it. No spec-side twin to file — the oracle is intact; the copy drifted.4 · §2.3 target table row 2 — a retired surface offered as a v17 home
The prescription names two live targets …and the rowmulti-source, multi-stage transformation→an **ETL transformation step**.The prescription names one live target; the rest is the business decision:and the row →**nothing** — the L2 ETL layer retired at 17, unexecuted. Do it where it runs: warehouse ELT, a flow, a job.The sharpest one. An upgrade skill for protocol 17 pointed a customer at a surface that was retired in protocol 17.
#6414retired the whole L2 ETL layer — "automation/etl.zod.tshad no executor, and the sync architecture doc was recommending it".packages/spec/src/automation/etl.zod.tsno longer exists; all nine ETL defs sit inRETIRED_DEFS_BY_MAJOR[17]; executed,@objectstack/spec/automationexports zeroETL*names.The replacement text is not invented — it is the CHANGELOG's own FROM → TO row for exactly this intent: "nothing. There is no replacement because there was never an implementation … Do the work where it runs (the destination warehouse's ELT, a
flow, a scheduled job)."And the real tombstone names one live target, not two — the import mapping. The second row was never in the prescription.
This exact defect has been paid down once already, in the spec: "fix(spec): the
retryDelayMstombstone no longer points an upgrading author at the retired ETL surface (#6630)". The published skill was the copy nobody re-checked.5 · §3.5 worked rejection — the withdrawn automatic-rewrite claim, republished
Run `os migrate meta --from 16` to rewrite it automatically.Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.The sample "what a tombstone looks like when it works" quoted a sentence the spec had already withdrawn. The real prescription (
packages/spec/src/shared/mapping.zod.ts, reproduced verbatim by PROBE 5) ends with the house form.retired-key.ts's docblock says why: the in-place AST codemod is separately commissioned, and "porting the listed edits into the project's own.tssources is the author's work".It also contradicted this skill's own §1 headline — "⚠ The one fact that surprises every operator:
os migrate metadoes not rewrite your source files."packages/spec/src/shared/retired-key-migrate-sentence.test.tspins against this claim class-wide via itsWITHDRAWN_CLAIMregex, which matches the removed text verbatim. It never fired: that test's markdown corpus is one hard-coded path, the internal.claude/skills/spec-property-retirement/SKILL.md. Carded as #13859.Two trims that pay for the above (net −3 lines, token-neutral)
The file sat at its ratchet ceiling with headroom 0, so every added word was funded by a deletion in the same passage. Both trims are inside the §2.3 block the corrections rewrite, and neither removes a fact:
…**no runtime ever executed any of them**, so nothing is lost by deleting the key — but the customer wrote it because they wanted a transformation, and that need is real even though the key never served it.…**no runtime ever executed any of them**. The customer wrote it because they wanted a transformation, and that need is real regardless.That third row is not a joke and it is frequently the truth: … Whether the downstream data is therefore wrong is a question only the owner can answer, and it is exactly the kind of finding the report exists to surface.That third row is frequently the truth: … Whether the downstream data is wrong is a question only the owner can answer — exactly the kind of finding the report exists to surface.Zero out-of-list changes. The diff is 15 insertions / 18 deletions in one file.
The two
skills/**readingsskills/objectstack-upgrade/SKILL.md— linescheck-skills-token-ratchet)skills/objectstack-upgrade/— linesThe package is one file, so the two readings coincide. Net line budget ≤ 0: satisfied (−3). Ceiling unchanged — no raise requested, none needed.
Executed evidence (non-vacuity control)
Probes ran against a freshly built
@objectstack/spec(pnpm --filter '@objectstack/spec^...' build && pnpm --filter @objectstack/spec build, VERDICT command-exit 0 underscripts/pm/os-verify-lock.sh), importing the builtdist/— not source.PROBE 3 — §3.3's headline claim, proven true by execution. The skill says the 16 → 17 crossing has exactly one migration-chain-only conversion: retired from the load path and leaving nothing to reject it, so
validatestays green while the chain still has work. Classifying that class needs a parse, not a grep — 46 of the 58 major-17 conversions areretiredFromLoadPath, and most of them ARE rejected by a tombstone. Driving each conversion's ownfixture.beforethroughObjectStackDefinitionSchema.safeParse:Exactly one. VERIFIED, and the count is right for the right reason.
PROBE 4 — the same conversion, and the evidence for correction 2.
Both halves of §3.3 confirmed — green
validatewith chain work outstanding — and the stamping hazard of an understated--fromdemonstrated in the same run.PROBE 5 — the real rejection, verbatim.
code: invalid_type,expected: "never", pathconnectors.0.fieldMappings.0.transform, message = the tombstone's own prescription ending "…to list the mechanical edits for existing sources; apply them by hand." Confirms the §3.5 sample's shape (invalid_type,expected: never, fix-it text as the error — VERIFIED) and its ending (FALSE, correction 5).PROBE 6 —
TransformTypeoptions["none","constant","lookup","split","join","javascript","map"](correction 3).The skill's own refresh command, run verbatim (§"How this table is refreshed"):
17.0.0 floor 10 | 16->17: 58 converted, 77 semantic. It works exactly as written, against the installed package, with no edit — VERIFIED.The chain, executed end to end on a protocol-16 source:
field-conditionalRequired-to-requiredWhenrewroteconditionalRequired→requiredWhenvalue-unchanged, andagent-tools-to-skillsremoved the inline tool list. Both are the shapes the file'sos:checkblock teaches as v17-canonical — VERIFIED.Verified, worth recording
17.0.0-rc.5" is a deliberately bounded snapshot with a stated refresh procedure and a stated tie-break ("the install wins"). Re-measured at@objectstack/spec17.2.0 — four publishes past the pin — every number has grown, which is the direction the design anticipates: D2 conversions 45 → 58, D3 semantic entries 29 → 77,retiredKey()tombstones 113/32 files → 179/44 files, distinct[REMOVED]prescriptions 96 → 145. Support floor still 10;PROTOCOL_VERSIONstill17.0.0. Left unedited on purpose — re-pinning is a maintainer call, not a sweep correction, and the deltas are recorded here so they stay legible.RETIRED_KEYS_BY_MAJOR[17]no longer containsdata/ExternalFieldMapping:transform—#8075retired the whole external-lookup family and the def retirement subsumed the key entry. That is a removal, the one direction the pin's contract does not describe ("registrations after that publish add entries"). It was correct at rc.5 (the retirement landed in 17.0.0 GA, after rc.6), so it is a stale pin, not a false claim. Flagged for whoever re-pins.applyConversionsToStoredItemreally is called by both seams the skill names —packages/metadata/src/loaders/database-loader.tsandpackages/objectql/src/plugin.ts— and really does pinincludeRetired: true. One narrowing the skill does not state:flowrows are skipped at those two seams and canonicalize atregisterFlowinstead, "with the same full-chain policy", so the conclusion the skill draws is true for every row type. Recorded, not corrected — no reader action changes and the budget was spent on facts that do.STORED_ONLY_FLAGSinpackages/cli/src/commands/migrate/meta.tsis exactlyapply,yes,force,type,database-url— the Failure-modes row matches member for member, and the mutual exclusion really is refused rather than ignored, in both directions.Applied N mechanical change(s),N manual change(s) require your judgment,Migrated stack is schema-valid,Nothing to migrate, thepath: from → to (conversionId)line, and thewhy:/verify:labels.packages/spec/package.jsonfilesisdist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json— so the conversion/migration registries really are not shipped andsrc/**/*.zod.tsreally is the onlysrcthat is, exactly as §2.1's callout says.MigrationFloorError's message matches the Failure-modes row;os validatereally carries a positional config arg and--strict, and really runs a schema pass plus an authoring-rule pass.NOT MEASURABLE (~40, recorded, never skipped)
.upgrade/workspace convention, the decide-alone-or-ask rules, the "how to ask" shape, the report template's five sections. Judgment prescriptions with no implementation to read.--applywrite path (needs a booted deployment DB),os migrate files-to-references/os migrate value-shapesenforcement effects (need a deployment each), the R2 renamed-context-field trap (needs a real dispatch). This is the class the flight-① addendum sized at ~20 min/claim and the roster now tracks for a dedicated harness flight.check:skill-examplespopulation: this package has exactly 1os:checkmarker, so its population is one fence — non-empty, control not needed (contrast flight ③'s empty-population rule). The block type-checks and its two taught renames were confirmed by execution above.Out-of-scope findings, filed unassigned
skills/**. Sibling of Gate candidate: named-identifier liveness check over skills/** — a skill row citing a schema/field that greps to zero repo-wide is a red, not a sentence #13678: the corpus grew, the gate's population did not.manifest.specVersion, whichManifestSchemadoes not declare #13860 — "which protocol major was this authored against?" has no declared home. Three CLI commands (validate,doctor,compile) advise offmanifest.specVersion, whichManifestSchemadoes not declare; the correction in item 1 points atmanifest.engines.protocolinstead. Carries a four-axis option analysis and a recommendation. Distinct from The blank template declares two protocol versions that disagree by eleven majors —specVersion: "^6.0.0"in the manifest,engines.protocol: '^17'in the config, and only the second is synced #9264 (already in a terminal state), which paid down the template side.Neither is touched here.
Gates
Derived from the real diff via
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(change set read by the script itself from the merge base; not a hand-built list) — 14 families, all re-run on the final commit414cbd368with a clean tree. Exit codes captured before any pipe.check-skills-token-ratchetprints the load-bearing line:skills/objectstack-upgrade/SKILL.md is 8333 tokens (ceiling 8333; headroom 0).Two families needed a note rather than a verdict:
node scripts/check-test-completeness.mjs— NOT MEASURED, by the gate's own instruction. It grades a savedturbo run testlog and the derived family names it with no argument; it exits 3 (deliberately distinct from a finding's 1) printing "the local reading for this gate is NOT MEASURED … It is not a red, and there is nothing here to fix." CI tees the log and passes it.check:doc-formula-expressionsfirst exited 1 withPREREQUISITE NOT MET("Nothing was measured … It is NOT a finding") because@objectstack/formulaand@objectstack/lintwere unbuilt in a fresh worktree. Built both under the verify lock, then it ran and passed — the 0 above is the real measurement.skip-changesetverified rather than assumed: the diff is a singleskills/**markdown file and publishes nothing from any package.Posture
Draft, human merge,
needs:contract-reviewattached to this PR and to card #13840 in the same stroke (批 #12 CONTENT limb) — ⛔ not self-cleared. Sibling flight ⑩ (#13841,skills/objectstack-ai/**) ran this same wave on a disjoint face; no file overlap.Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
Generated by Claude Code
Generated by Claude Code