objectstack-upgrade
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUpgrading an ObjectStack metadata project across a protocol major
ObjectStack元数据项目跨协议大版本升级
This skill turns one session into an upgrade agent working on somebody
else's metadata project. It has one job: take a project authored against
protocol and leave it authored against the current major, with the change
proved rather than asserted.
Npreflight → mechanical chain → semantic residue → acceptance report
The upgrade is deliberately split into three layers, and the split is the whole
design. Two of them are not yours.
| Layer | Who owns it | What it is |
|---|---|---|
| 1 · Mechanical | The CLI. You invoke it, you never re-implement it. | |
| 2 · Semantic residue | You, with the project's owner. | Everything a conversion cannot express: intent choices, custom code calling retired APIs, prose that still teaches the old shape. |
| 3 · Acceptance | The gates. | Typed + parse-gated metadata, a green |
本技能将单次会话转化为针对他人元数据项目的升级代理。它只有一项任务:将基于协议开发的项目升级为基于当前大版本开发,并验证变更有效性而非仅声明完成。
N预检 → 机械转换链 → 语义残留处理 → 验收报告
升级流程被刻意拆分为三层,这种拆分是整个设计的核心。其中两层无需你手动操作。
| 层级 | 负责方 | 说明 |
|---|---|---|
| 1 · 机械层 | CLI工具。你只需调用它,绝不要重新实现。 | |
| 2 · 语义残留 | 你与项目负责人 | 所有转换链无法处理的内容:意图选择、调用已废弃API的自定义代码、仍沿用旧格式的说明文档。 |
| 3 · 验收层 | 校验机制 | 经过类型检查和解析校验的元数据、绿色 |
⛔ The boundary — read this before the first command
⛔ 边界规则 —— 执行第一条命令前请务必阅读
Never hand-write a rewrite the chain already applies. If a key was renamed,
the conversion table knows the rename; running the chain attributes each rewrite
to a hop and proves the result is schema-valid. A hand-edit does neither, and it
silently diverges the moment the chain gains an entry.
Never add a tolerant read to make old metadata load. No alias, no
"accept both spellings" branch, no coercion in the project's own code. A key was
retired because nothing enforced it or because exactly one spelling survives;
re-admitting the old one at the consumer is how the defect the retirement closed
comes back inside the customer's repo, where no gate can see it.
??Never resolve a residue item by guessing the owner's intent. The residue
exists precisely because it is a business decision. The rule for when you decide
alone and when you ask is in Layer 2 — it is the most
important paragraph in this skill.
Never report "upgraded" without the acceptance artifacts. green
plus the report is the machine criterion. Absent either, the status is
in progress, whatever the diff looks like.
validate绝不要手动编写转换链已能自动处理的重写内容。如果某个键被重命名,转换表已记录该重命名规则;运行转换链可追溯每一处重写的来源,并验证结果符合 Schema 规范。手动编辑无法实现以上两点,且一旦转换链新增规则,手动编辑的内容会悄然偏离标准。
绝不要添加兼容逻辑来加载旧版元数据。不要使用别名、“兼容两种拼写”的分支,也不要在项目代码中添加强制转换。某个键被废弃,要么是因为没有机制强制其使用,要么是因为仅有一种拼写保留;在消费端重新允许旧键使用,会让废弃本应修复的缺陷重新出现在客户仓库中,且无法被校验机制发现。
??绝不要通过猜测负责人意图来处理残留项。残留项存在的原因正是因为它需要业务决策。何时自行决策、何时咨询负责人的规则请查看第2层——这是本技能中最重要的段落。
绝不要在没有验收成果的情况下报告“已升级”。绿色校验结果加报告是机器判定的标准。缺少任意一项,无论差异内容如何,状态都为进行中。
validateQuickstart
快速开始
bash
undefinedbash
undefined0 · preflight — what major is this project on, what major is installed?
0 · 预检 —— 项目当前使用的大版本是什么?已安装的大版本是什么?
grep -rn "protocol" objectstack.config.ts package.json | head
node -p "require('@objectstack/spec/package.json').version"
grep -rn "protocol" objectstack.config.ts package.json | head
node -p "require('@objectstack/spec/package.json').version"
1 · mechanical — replay the chain (reads the config, writes nothing but --out)
1 · 机械层 —— 执行转换链(读取配置,仅通过--out参数输出内容)
os validate > .upgrade/validate-before.txt 2>&1 || true # the control, kept
os migrate meta --from 16 --step
os migrate meta --from 16 --json > .upgrade/migrate.json
os migrate meta --from 16 --out .upgrade/migrated.stack.json
os validate > .upgrade/validate-before.txt 2>&1 || true # 保留初始校验结果作为对照
os migrate meta --from 16 --step
os migrate meta --from 16 --json > .upgrade/migrate.json
os migrate meta --from 16 --out .upgrade/migrated.stack.json
2 · semantic residue — harvest the prescriptions, then work each item
2 · 语义残留 —— 获取提示信息,然后逐一处理
node -e "console.log(require('fs').readFileSync(require.resolve('@objectstack/spec/package.json').replace('package.json','spec-changes.json'),'utf8'))" > .upgrade/spec-changes.json
grep -rho '[REMOVED][^"]*' node_modules/@objectstack/spec/json-schema/ | sort -u > .upgrade/tombstones.txt
node -e "console.log(require('fs').readFileSync(require.resolve('@objectstack/spec/package.json').replace('package.json','spec-changes.json'),'utf8'))" > .upgrade/spec-changes.json
grep -rho '[REMOVED][^"]*' node_modules/@objectstack/spec/json-schema/ | sort -u > .upgrade/tombstones.txt
3 · acceptance — all four, not three
3 · 验收层 —— 完成全部四项,缺一不可
os validate # green (compare against validate-before.txt)
tsc --noEmit # tombstones type the retired keys as
os migrate meta --from 17 # must say "Nothing to migrate"
neveros validate # 绿色通过(与validate-before.txt对比)
tsc --noEmit # 废弃键的类型被标记为
os migrate meta --from 17 # 必须提示“Nothing to migrate”
never→ write .upgrade/REPORT.md (template in §3.4)
→ 编写.upgrade/REPORT.md(模板见§3.4)
Everything below is the long form of those four steps.
---
以下内容是上述四个步骤的详细说明。
---0 · Preflight
0 · 预检
Establish the FROM major — do not guess it
确定源大版本 —— 不要猜测
--from- in the stack config (
manifest.protocol→'16.0.0'). This is the declared answer and the kernel checks it at load time.--from 16 - The last major the project ever installed — read the lockfile history (
@objectstack/spec) when the manifest is absent or stale.git log -p pnpm-lock.yaml | grep -m5 '@objectstack/spec' - Ask. A manifest that says 16 on a project last touched two years ago is a
claim, not a measurement. If (1) and (2) disagree, the lower one is the safe
— the chain is idempotent, so replaying a hop that has already been applied is a no-op, while skipping a hop loses its rewrites.
--from
Arriving several majors late is the designed-for case.
replays every step in order; there is no penalty for lateness and no requirement
to upgrade one major at a time.
os migrate meta --from 10--from- 栈配置中的(如
manifest.protocol对应'16.0.0')。这是官方声明的版本,内核在加载时会校验此值。--from 16 - 项目曾安装的最后一个大版本——当清单缺失或过时,可查看锁文件历史记录(
@objectstack/spec)。git log -p pnpm-lock.yaml | grep -m5 '@objectstack/spec' - 咨询负责人。如果两年未维护的项目清单显示版本为16,这只是一个声明而非实际情况。如果(1)和(2)结果不一致,选择较低的版本作为参数值——转换链是幂等的,重复执行已完成的步骤不会产生影响,而跳过步骤会丢失对应的重写内容。
--from
跨多个大版本升级是设计时就支持的场景。会按顺序执行所有步骤;跨版本升级没有额外成本,也无需逐版本升级。
os migrate meta --from 10Make the work reviewable before you change anything
在变更前确保工作可审核
bash
git checkout -b upgrade/protocol-17
mkdir -p .upgrade # every artifact this skill produces lands hereThe directory is the deliverable's workspace: the machine outputs
(, , , )
and the human output (). Keeping them in the repo for the review, and
deleting them on merge, is the usual arrangement — decide it with the owner.
.upgrade/migrate.jsonmigrated.stack.jsonspec-changes.jsontombstones.txtREPORT.mdbash
git checkout -b upgrade/protocol-17
mkdir -p .upgrade # 本技能生成的所有成果都存放在此目录.upgrade/migrate.jsonmigrated.stack.jsonspec-changes.jsontombstones.txtREPORT.md1 · Mechanical layer — invoke the chain
1 · 机械层 —— 调用转换链
What os migrate meta
actually does
os migrate metaos migrate meta
的实际作用
os migrate metabash
os migrate meta --from 16 # replay 16 → current
os migrate meta --from 16 --step # per-hop checkpoint (bisect a failure)
os migrate meta --from 16 --to 17 # stop at a specific major
os migrate meta --from 16 --json # machine-readable result
os migrate meta --from 16 --out migrated.json # write the canonicalized stackIt loads the stack config, normalizes it without applying the load-time
conversion pass, replays each major's conversions as a chain hop, and then
parses the result against the current schema to prove the output is valid. What
it prints:
- — one line per rewritten site, as
Applied N mechanical change(s). This is the diff, already attributed.path: from → to (conversionId) - — the chain's semantic entries for the majors you crossed, each with a
N manual change(s) require your judgmentand awhyline. These are Layer 2's input, not a warning to dismiss.verify - — or the warning that it is not yet, which means a residue item is still blocking the parse.
Migrated stack is schema-valid - Pending data migrations, when the chain crosses into a major with per-deployment data gates — see below.
bash
os migrate meta --from 16 # 执行16 → 当前版本的转换
os migrate meta --from 16 --step # 分步执行(用于排查故障)
os migrate meta --from 16 --to 17 # 升级到指定大版本后停止
os migrate meta --from 16 --json # 输出机器可读的结果
os migrate meta --from 16 --out migrated.json # 输出标准化后的栈配置该命令加载栈配置,不应用加载时转换流程,按顺序执行每个大版本的转换步骤,然后将结果与当前Schema进行解析校验,以验证输出内容有效。输出信息包括:
- —— 每一行对应一处重写,格式为
Applied N mechanical change(s)。这就是差异内容,且已追溯来源。path: from → to (conversionId) - —— 转换链针对你跨越的大版本生成的语义条目,每个条目包含
N manual change(s) require your judgment和why说明。这些是第2层的输入内容,而非可忽略的警告。verify - —— 或提示结果尚未符合规范,这意味着仍有残留项阻碍解析。
Migrated stack is schema-valid - Pending data migrations —— 当转换链跨越到包含部署数据校验的大版本时会显示此信息——详见下文。
⚠ The one fact that surprises every operator
⚠ 让所有操作者意外的事实
os migrate meta--outThis is deliberate: rewriting a TypeScript config through an AST is lossy — it
drops comments, reorders keys, and cannot see values that come from imports or
expressions. So the mechanical layer gives you a provably valid target and
the attributed list of edits, and porting those edits into the project's own
sources is yours. Work from the printed list, one at a time; use
as the oracle you diff against, never as the file you ship.
conversionId--outbash
os migrate meta --from 16 --out .upgrade/migrated.stack.jsonos migrate meta--out这是刻意设计的:通过AST重写TypeScript配置会丢失注释、打乱键的顺序,且无法识别来自导入或表达式的值。因此机械层会为你提供一个经验证有效的目标配置和带来源追溯的编辑列表,将这些编辑内容移植到项目源文件中是你的工作。请按照输出的列表逐一处理,每个对应一项;将输出的内容作为对比基准,绝不要直接作为交付文件。
conversionId--outbash
os migrate meta --from 16 --out .upgrade/migrated.stack.jsonthen, after porting the edits into the real sources:
将编辑内容移植到实际源文件后:
os migrate meta --from 17 --out .upgrade/recheck.json # should apply 0 changes
That last line is the cheapest possible proof that the port is complete: replay
the chain from the *target* major and it must find nothing to do.os migrate meta --from 17 --out .upgrade/recheck.json # 应提示0项变更
最后一行是验证移植是否完成的最简方式:从目标大版本重新执行转换链,应无任何变更。Stored rows: rehydration replays the same conversions
存储行:重新加载时会执行相同的转换
A deployment's rows are the other subject. They are handled for
you at read time — the metadata loader and the ObjectQL plugin both pass each
stored row through , which replays the conversion
chain over a single item including entries retired from the load path. A row
written under protocol 16 therefore rehydrates in its protocol-17 shape without
anybody editing it.
sys_metadataapplyConversionsToStoredItemWhat that does and does not mean:
-
You do not hand-edit. Ever. A row at rest has no author to ask, so the replay is unconditional and complete by design.
sys_metadata -
The rows on disk stay in their old shape until something rewrites them. Rehydration is a read-time projection.
-
To make it durable, run the stored pass — read-only by default:bash
os migrate meta --stored # preview, writes nothing os migrate meta --stored --type view --type object # narrow the pass os migrate meta --stored --apply # rewrite the rows (prompts)takes no--stored: a stored row carries its own history, so the pass replays the whole chain. The authored-source flags and the stored-only flags are mutually exclusive, and mixing them is refused rather than ignored.--from
部署中的行是另一处理对象。它们会在读取时自动处理——元数据加载器和ObjectQL插件会将每个存储行传入,对单个条目执行完整的转换链包括加载路径中已废弃的条目。因此,基于协议16写入的行会自动以协议17的格式重新加载,无需手动编辑。
sys_metadataapplyConversionsToStoredItem这意味着:
-
绝不要手动编辑。存储的行没有可咨询的作者,因此重新加载时会无条件执行完整的转换。
sys_metadata -
磁盘上的行仍保留旧格式,直到被重写。重新加载是读取时的投影转换。
-
如需持久化更新,执行存储转换——默认是只读模式:bash
os migrate meta --stored # 预览,不写入内容 os migrate meta --stored --type view --type object # 缩小转换范围 os migrate meta --stored --apply # 重写行(会提示确认)无需指定--stored:存储的行包含自身的历史信息,因此转换会执行完整的链。源文件标记和仅存储标记互斥,混合使用会被拒绝。--from
Data migrations are not metadata migrations
数据迁移≠元数据迁移
When the chain crosses into a major carrying per-deployment data gates, the
command ends by naming them — for the 16 → 17 crossing, and only when the
project's own metadata declares the field classes each gate is about:
| Command | What staying un-run costs |
|---|---|
| Media values only warn instead of being enforced, and released files are never collected. |
| Stored reference and structured-JSON values are not checked against their field contracts; a malformed value only warns. |
Both are dry-run by default; is the only writing mode. They run
against each deployment's database, once per deployment, and nothing in the
metadata upgrade can run them or tell whether they have run. Not running them is
safe — enforcement simply stays off. Carry them into the report as pending, by
name, so a gate nobody was told about is not served by nobody.
--apply当转换链跨越到包含部署数据校验的大版本时,命令会列出相关校验——以16 → 17为例,仅当项目元数据声明了每个校验对应的字段类时才会显示:
| 命令 | 未执行的影响 |
|---|---|
| 媒体值仅发出警告而非强制校验,已发布文件不会被回收。 |
| 存储的引用和结构化JSON值不会按字段契约校验,格式错误的值仅发出警告。 |
两者默认都是试运行模式;只有是写入模式。它们针对每个部署的数据库执行,每个部署执行一次,元数据升级无法自动执行这些命令或判断是否已执行。未执行是安全的——只是校验机制不会启用。请在报告中按名称标记为待处理,避免相关校验机制被遗漏。
--apply2 · Semantic residue — the part that is yours
2 · 语义残留 —— 你需要处理的部分
A conversion can rename a key, drop a dead one, or lift a value onto its
declared block. It cannot make a decision. Everything it cannot do lands here.
转换链可以重命名键、删除废弃键,或将值提升到指定区块,但无法做决策。所有转换链无法处理的内容都属于这一层。
2.1 Harvest the instruction sources — all of them ship
2.1 获取所有提示来源 —— 均随包发布
The prescriptions are not on a docs site you have to be online for. They ship
inside the installed package. Measured against the published
file list:
@objectstack/spec| Source | Where, in a consumer project | Carries |
|---|---|---|
| Chain result | | The conversions + semantic entries for exactly the majors you cross. Start here — it is computed from the installed spec, so it can never be stale. |
| D4 projection | | The same data for every major, offline: |
| Tombstone prescriptions | | Every retired key's |
| FROM → TO tables | | The per-retirement narrative, including the "what to write instead" table. This is why the package ships its changelog. |
| The error itself | Your parse / | The same prescription string, delivered at the moment you hit it. |
bash
undefined提示信息不依赖在线文档,而是随安装的包一起发布。基于已发布的文件列表:
@objectstack/spec| 来源 | 在消费项目中的位置 | 包含内容 |
|---|---|---|
| 转换链结果 | | 针对你跨越的大版本的转换规则+语义条目。从此处开始——它基于已安装的spec生成,绝不会过时。 |
| D4投影 | | 所有大版本的相同数据,支持离线使用: |
| 废弃提示 | | 所有废弃键的 |
| 版本对照表 | | 每个废弃项的说明,包括“替代方案”表格。这也是包中包含变更日志的原因。 |
| 错误信息本身 | 你的解析/ | 相同的提示字符串,在触发错误时直接显示。 |
bash
undefinedevery tombstone prescription the installed spec carries, deduped
获取已安装spec中的所有废弃提示,去重
grep -rho '[REMOVED][^"]*' node_modules/@objectstack/spec/json-schema/ | sort -u
grep -rho '[REMOVED][^"]*' node_modules/@objectstack/spec/json-schema/ | sort -u
the FROM → TO table for one retired key
获取某个废弃键的版本对照表
grep -n -B4 -A20 'transform' node_modules/@objectstack/spec/CHANGELOG.md | less
> **Not reachable from a consumer project**, so do not send anyone there: the
> conversion and migration registries (`src/conversions/registry.ts`,
> `src/migrations/registry.ts`) and the platform repo's generated upgrade guide
> are **not** in the published package — only `src/**/*.zod.ts` is. Their
> consumer-facing projection is `spec-changes.json` and the chain's own `--json`
> output, which is exactly what the table above points at.grep -n -B4 -A20 'transform' node_modules/@objectstack/spec/CHANGELOG.md | less
> **消费项目无法访问**,因此不要引导任何人前往:转换和迁移注册表(`src/conversions/registry.ts`、`src/migrations/registry.ts`)以及平台仓库生成的升级指南**未包含在发布包中**——仅`src/**/*.zod.ts`会包含。面向消费者的投影内容是`spec-changes.json`和转换链的`--json`输出,正是上表指向的内容。2.2 The three residue classes
2.2 三类残留项
R1 · Intent choice — a key was retired with no single lossless target.
The conversion drops the key (so the project parses) and, where it matters,
emits a notice naming the site. What the key was for still has to go
somewhere, and where is a business statement.
R2 · Custom code calling a retired API. The chain's semantic entries name
these: a service slot that no longer exists, an engine method that was removed,
a context field that was renamed. Metadata parses fine; the project's own
TypeScript is what breaks — or worse, keeps compiling while reading .
undefinedR3 · Prose that still teaches the old shape. READMEs, comments, ADRs, seed
fixtures, and the project's own AI conventions file. Nothing fails, and the next
agent to read the repo re-authors the retired shape from it.
R1 · 意图选择 —— 某个键被废弃且无唯一无损替代方案。转换链会删除该键(确保项目可解析),并在重要位置发出提示,标记该位置。该键的用途仍需找到替代方案,而选择是业务决策。
R2 · 调用已废弃API的自定义代码。转换链的语义条目会列出这些内容:不存在的服务插槽、已移除的引擎方法、已重命名的上下文字段。元数据可正常解析,但项目自身的TypeScript代码会报错——更糟的是,代码可能仍能编译但读取值。
undefinedR3 · 沿用旧格式的说明文档。README、注释、ADR、种子测试数据以及项目自身的AI约定文件。不会导致报错,但后续读取仓库的代理会根据这些内容重新编写废弃格式的代码。
2.3 A worked R1 — the retired field-mapping transform
transform2.3 R1示例 —— 已废弃的字段映射transform
transformThe shape in a protocol-16 project:
jsonc
{
"connectors": [{
"name": "sap_erp",
"fieldMappings": [
{ "source": "order_value", "target": "order_total",
"transform": { "type": "javascript", "expression": "value / 100" } }
]
}]
}The chain deletes the key () and the schema
tombstones it, so the parse error is the prescription: the union had five
members and 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.
field-mapping-transform-removedThe prescription names two live targets, and choosing between them is the
business decision:
| If the intent was… | The v17 home is… |
|---|---|
| per-row value shaping on an import | Import mapping |
| multi-source, multi-stage transformation | an ETL transformation step. |
| nothing — the value was already correct | delete the key and record that the transformation never ran. |
That third row is not a joke and it is frequently the truth: the member never
executed, so the connector has been landing raw values for as long as it has
been running. 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.
<a id="decide-alone-or-ask"></a>
协议16项目中的格式:
jsonc
{
"connectors": [{
"name": "sap_erp",
"fieldMappings": [
{ "source": "order_value", "target": "order_total",
"transform": { "type": "javascript", "expression": "value / 100" } }
]
}]
}转换链会删除该键(),Schema会标记其为废弃,因此解析错误本身就是提示:该联合类型曾有五个成员,但没有运行时执行过其中任何一个,因此删除该键不会丢失任何内容——但客户编写该键是因为需要转换功能,这个需求是真实存在的,尽管该键从未发挥作用。
field-mapping-transform-removed提示信息列出了两个可用的替代方案,选择哪一个是业务决策:
| 如果意图是… | v17中的对应位置是… |
|---|---|
| 导入时逐行值转换 | 导入映射 |
| 多源多阶段转换 | ETL转换步骤。 |
| 无需求——值已正确 | 删除该键,并记录该转换从未执行过。 |
第三点并非玩笑,且经常是实际情况:该成员从未执行过,因此连接器一直以来都在传输原始值。下游数据是否因此错误,只有负责人能回答,这正是报告需要呈现的发现之一。
<a id="decide-alone-or-ask"></a>
2.4 Decide alone, or ask the owner
2.4 自行决策或咨询负责人
Decide it yourself when all three hold:
- The prescription names exactly one target. The tombstone or conversion summary gives a single FROM → TO, with the value unchanged.
- The evidence is in the project. A grep in the repo settles it — the skill that already owns the tool, the import that already exists, the field the predicate already references.
- Being wrong fails a gate. A mistaken choice breaks or
tscrather than changing behaviour quietly.validate
Ask the owner when any one of these holds:
- Two or more real targets, and the choice is a business statement — the
case above.
transform - The change is observable without a test failing — security posture (an authentication default), row visibility (an access predicate), retry counts, retention. A wrong call here ships silently and is discovered by an auditor.
- Capability has to be re-declared somewhere new, so choosing wrong removes a capability instead of breaking a build. Agent tooling that has to move inside a specific skill is the canonical shape.
- The source is dead or undocumented in their repo — nothing to decide from. Say so; do not invent a rationale.
How to ask. One message, per item, carrying: the site (file and path), the
prescription verbatim, the options with what each costs, your recommendation and
why, and what you will verify once they choose. Never a bare "how should I
handle ?" — that hands the reading work back to the person with the
least context about the diff.
transformWhile you wait, do not stall the upgrade. Park the item in the report as
, keep the mechanical layer complete, and keep going. A
project can be schema-valid with open residue items; it just is not done.
AWAITING DECISION满足以下全部三点时可自行决策:
- 提示信息指定唯一替代方案。废弃提示或转换摘要给出唯一的FROM → TO映射,且值保持不变。
- 项目中有明确证据。通过grep查询仓库可确认——工具所属的技能、已存在的导入、谓词已引用的字段。
- 决策错误会触发校验失败。错误选择会导致或
tsc失败,而非悄然改变行为。validate
满足以下任意一点时需咨询负责人:
- 存在多个有效替代方案,选择属于业务决策——如上述案例。
transform - 变更可被观察但不会触发测试失败——安全策略(认证默认值)、行可见性(访问谓词)、重试次数、保留期限。错误决策会悄然生效,直到被审计发现。
- 能力需要在新位置重新声明,错误选择会移除某项功能而非导致构建失败。例如,代理工具必须迁移到特定技能内部的典型场景。
- 项目中该来源已失效或无文档——无决策依据。如实说明,不要编造理由。
咨询方式。针对每个残留项发送一条消息,包含:位置(文件和路径)、完整提示信息、各选项的影响、你的推荐及理由,以及选择后你将如何验证。绝不要只发送“我该如何处理?”——这会把阅读工作推给最不了解差异上下文的人。
transform等待回复期间不要暂停升级。在报告中将该项目标记为,保持机械层完成,继续处理其他内容。项目可在存在未处理残留项的情况下符合Schema规范,但不算完成。
AWAITING DECISION2.5 Working an R2 — retired APIs in the project's own code
2.5 处理R2 —— 项目代码中已废弃的API
The chain's semantic entries are the search list. For each one, the surface it
names is a string you grep for in the project's own source:
bash
undefined转换链的语义条目是搜索列表。针对每个条目,搜索项目自身源代码中对应的字符串:
bash
undefinedfrom the chain's own output — the surfaces it says it cannot fix for you
从转换链输出中获取无法自动修复的内容
os migrate meta --from 16 --json | node -e "
let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
for (const t of JSON.parse(s).todos) console.log(t.surface);
})"
os migrate meta --from 16 --json | node -e "
let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
for (const t of JSON.parse(s).todos) console.log(t.surface);
})"
then, for each surface, search the project (not node_modules)
针对每个内容,搜索项目源代码(排除node_modules)
grep -rn "<surface-token>" src/ app/ --include='.ts' --include='.tsx'
Two traps that have cost real upgrades a lap:
- **A renamed context field keeps compiling.** When a read moves from one key to
another and the old key is simply absent afterwards, the code reads
`undefined` and every branch quietly takes its false path. Verify against a
real dispatch, not a fixture — invoke the path and assert the value observed
under the canonical key.
- **A rename is not always the fix.** If the old read was itself wrong, renaming
it migrates the defect rather than the code. Read the semantic entry's
`reason` before applying its `replacement`.grep -rn "<surface-token>" src/ app/ --include='.ts' --include='.tsx'
两个曾导致升级返工的陷阱:
- **已重命名的上下文字段仍能编译**。当读取从旧键迁移到新键后,旧键不存在,代码会读取`undefined`,所有分支会悄然走错误路径。请针对实际调度进行验证,而非测试用例——触发该路径并断言规范键下的观测值。
- **重命名并非总是解决方案**。如果旧读取本身就错误,重命名会迁移缺陷而非代码。在应用替代方案前,请阅读语义条目的`reason`说明。2.6 Working an R3 — the prose sweep
2.6 处理R3 —— 文档扫描
Run it last, once the shapes are settled, and run it over the whole repo:
bash
undefined在格式确定后最后执行,扫描整个仓库:
bash
undefinedevery retired key name the installed spec knows, as a search list
获取已安装spec中所有废弃键名,作为搜索列表
grep -rho '[REMOVED] ](.)`.*/\1/' | sort -u > .upgrade/retired-names.txt
[^' node_modules/@objectstack/spec/json-schema/ \ | sed 's/.*
Then sweep the project's `*.md`, comments, seed fixtures, and its AI conventions
file. The conventions file matters most: it is what the next agent loads before
it writes anything, so a retired shape left there re-enters the codebase on the
next feature, long after the upgrade closed.
---grep -rho '[REMOVED] ](.)`.*/\1/' | sort -u > .upgrade/retired-names.txt
[^' node_modules/@objectstack/spec/json-schema/ \ | sed 's/.*
然后扫描项目中的`*.md`文件、注释、种子测试数据以及AI约定文件。约定文件最为重要:它是后续代理编写代码前加载的内容,因此残留的废弃格式会在后续功能开发时重新进入代码库,远在升级完成之后。
---3 · Acceptance — what "upgraded" means
3 · 验收层 —— “已升级”的定义
Four artifacts. Three are machine-checked; the fourth is the one a human reads.
四项成果。三项由机器校验;第四项供人类阅读。
3.1 Typed
3.1 类型检查通过
bash
tsc --noEmitA retired key is not merely absent from the schema — it is declared as a
tombstone whose input type is . Assigning anything to it fails to
compile, at the authoring site, before anything runs. A green is therefore
positive evidence that no retired key survives in typed sources.
nevertscbash
tsc --noEmit废弃键不仅从Schema中移除,还被声明为类型的废弃项。为其赋值会在编写阶段编译失败,无需运行。绿色结果是类型源文件中无废弃键存活的明确证据。
nevertsc3.2 Parse-gated
3.2 解析校验通过
The same tombstone rejects at parse time, and the rejection carries the
prescription rather than a generic "unrecognized key". This is the channel that
catches metadata cannot see: JSON files, database rows, anything built at
runtime. You do not have to do anything to enable it — but you do have to
prove it is live for this project, because a schema that silently strips is
indistinguishable from one that accepts. See
the reverse check.
tsc相同的废弃项会在解析时被拒绝,且拒绝信息包含提示内容而非通用的“无法识别的键”。这一机制会捕获无法检测的元数据:JSON文件、数据库行、任何运行时构建的内容。你无需手动启用——但必须验证该机制对当前项目生效,因为静默剥离的Schema与接受旧键的Schema无法区分。详见反向校验。
tsc3.3 Validate
3.3 Validate校验通过
bash
os validate # green is the criterion
os validate --strict # warnings become errors — agree with the owner whether this is the baros validateos validate⚠ A green
does NOT mean the chain has nothing left to dovalidateSome conversions are migration-chain-only: the loader deliberately does not apply them and no tombstone rejects the old shape, because the change is a default flip rather than a rename — auto-applying it would stamp a constraint onto sources that deliberately omit it. The 16 → 17 crossing has one:, which writes the physicalfield-required-notnull-explicitthatstorage.notNullused to imply on its own.requiredA project carrying only that shape validates green while the chain still has work. Sogreen is necessary and not sufficient, and the criterion that closes the gap is the replay:validatebashos migrate meta --from <target-major> # must report "Nothing to migrate"Run both. A report that cites onlycannot see this class at all.validate
bash
os validate # 绿色通过是判定标准
os validate --strict # 警告转为错误——与负责人协商是否以此为标准os validateos validate⚠ 绿色
不代表转换链已完成所有工作validate部分转换是仅迁移链执行:加载器刻意不应用这些转换,且废弃项不会拒绝旧格式,因为变更是默认值翻转而非重命名——自动应用会给刻意省略该约束的源文件添加限制。16 → 17升级包含一项此类转换:,它会写入field-required-notnull-explicit曾隐含的物理约束required。storage.notNull仅包含此类格式的项目会通过校验绿色,但转换链仍有工作要做。因此绿色validate是必要非充分条件,填补缺口的判定标准是重新执行转换链:validatebashos migrate meta --from <target-major> # 必须提示“Nothing to migrate”请同时执行两者。仅引用结果的报告无法覆盖此类情况。validate
3.4 The report — the human half
3.4 报告 —— 人类可读部分
The upgrade is not finished by a passing command; it is finished by a document a
maintainer can read in five minutes and a year from now. Write
:
.upgrade/REPORT.mdmarkdown
undefined升级并非通过某个命令完成,而是通过一份维护者可在5分钟内读完、且一年后仍能理解的文档完成。编写:
.upgrade/REPORT.mdmarkdown
undefinedProtocol 16 → 17 upgrade — <project>
协议16 → 17升级 —— <项目名称>
Status: complete | complete with N open decisions
Spec: <installed @objectstack/spec version> · Chain: 16 → 17
Verified: green · green · replay-from-17 applies 0 changes
os validatetsc --noEmit状态: 已完成 | 已完成,存在N项待决策内容
Spec版本: <已安装的@objectstack/spec版本> · 转换链: 16 → 17
验证结果: 绿色 · 绿色 · 从17版本重新执行转换链无变更
os validatetsc --noEmit1 · Mechanical (applied by the chain)
1 · 机械层变更(由转换链执行)
| Site | Change | Conversion |
|---|---|---|
| | |
| … |
N sites, M conversions. Ported into sources from .
os migrate meta --out| 位置 | 变更内容 | 转换ID |
|---|---|---|
| | |
| … |
共N处位置,M项转换。已从输出内容移植到源文件。
os migrate meta --out2 · Semantic residue (decided)
2 · 语义残留处理(已决策)
connector.fieldMappings[].transform
— RESOLVED
connector.fieldMappings[].transformconnector.fieldMappings[].transform
—— 已解决
connector.fieldMappings[].transform- Site:
src/connectors/sap.ts:24 - Prescription: <verbatim from the tombstone>
- Options: import-mapping · ETL step · delete
transform - Decision: delete — owner confirmed the values arrive pre-scaled. Decided by: <who>, <date>.
- Verified: green; connector sync run against staging, 200 rows, values unchanged.
os validate
- 位置:
src/connectors/sap.ts:24 - 提示信息: <废弃提示原文>
- 选项: 导入映射· ETL步骤 · 删除
transform - 决策: 删除 —— 负责人确认值已预先缩放。 决策人: <姓名>, <日期>.
- 验证: 绿色;连接器同步运行于预发布环境,200行数据,值无变化。
os validate
3 · Open decisions
3 · 待决策内容
| Item | Site | Options | Recommendation | Blocking? |
|---|---|---|---|---|
| | | | no — parses without it |
| 项 | 位置 | 选项 | 推荐方案 | 是否阻塞? |
|---|---|---|---|---|
| | | | 否 —— 可正常解析 |
4 · Pending, per deployment
4 · 待执行的部署级任务
- — media values only warn until it passes.
os migrate files-to-references - — stored reference/JSON values unchecked until it passes.
os migrate value-shapes - — rows rehydrate correctly today; this makes it durable.
os migrate meta --stored --apply
- —— 媒体值仅发出警告,直到执行完成。
os migrate files-to-references - —— 存储的引用/JSON值未校验,直到执行完成。
os migrate value-shapes - —— 当前行可正确重新加载;此操作会持久化更新。
os migrate meta --stored --apply
5 · Not changed, and why
5 · 未变更项及原因
- <retired surface the project never used> — no occurrences.
Section 5 earns its place: "we looked and it was not there" is a finding, and
without it the next reader cannot tell a surface that was checked from one that
was missed.
<a id="reverse-check"></a>- <项目从未使用的废弃内容> —— 无匹配项。
第5部分至关重要:“我们已检查且未发现匹配项”是一项发现,没有这部分内容,后续读者无法区分已检查的内容和遗漏的内容。
<a id="reverse-check"></a>3.5 Prove the gate is real, do not assume it
3.5 验证校验机制有效,不要假设
Before you report the parse gate as acceptance evidence, make it fire once.
Take a value the chain would have converted, put it back after migrating, and
parse:
bash
undefined在报告中将解析校验作为验收证据前,请先触发一次校验失败。将转换链本应转换的值在迁移后恢复,然后执行解析:
bash
undefineda stack that still carries a retired key, fed straight to the schema
包含废弃键的栈配置,直接传入Schema校验
os validate .upgrade/residue-probe.config.mjs
Predict the direction **before** you run it. There are three real outcomes and
you must say which you expect:
1. **Rejected with the prescription** — a tombstoned key. This is what a
tombstone looks like when it works:
✗ connectors.0.fieldMappings.0.transform
invalid_type: … was removed in @objectstack/spec
17.0.0 (#5552, ADR-0049) … Delete the key. The transform pipeline that IS
enforced is the import mapping's … Run to
rewrite it automatically.
expected: never
FieldMapping.transformos migrate meta --from 16
Note what the error is not: not "unrecognized key", not a deprecation label.
The fix-it text *is* the error.
2. **Accepted, and the chain rewrites it** — a conversion with a live load-path
acceptance window. Your evidence for that key is the chain's diff, not the
parse gate.
3. **Accepted, and the chain still rewrites it** — a migration-chain-only
conversion (§3.3). `validate` cannot see this class at all; only the replay
can.
Record which one you actually got. A check whose expected direction you did not
state in advance proves nothing, and "it passed" is a different fact in each of
the three cases.
---os validate .upgrade/residue-probe.config.mjs
在执行前**预测结果方向**。存在三种实际结果,你必须说明预期的结果:
1. **被拒绝并显示提示信息**——废弃键。这是废弃机制生效的表现:
✗ connectors.0.fieldMappings.0.transform
invalid_type: … was removed in @objectstack/spec
17.0.0 (#5552, ADR-0049) … Delete the key. The transform pipeline that IS
enforced is the import mapping's … Run to
rewrite it automatically.
expected: never
FieldMapping.transformos migrate meta --from 16
注意错误信息的形式:不是“无法识别的键”,也不是弃用标签。修复提示本身就是错误信息。
2. **被接受,且转换链会重写它**——转换存在加载路径接受窗口期。该键的证据是转换链的差异,而非解析校验。
3. **被接受,且转换链仍会重写它**——仅迁移链执行的转换(§3.3)。`validate`无法检测此类情况;只有重新执行转换链可以。
记录实际得到的结果。未预先说明预期方向的校验无法证明任何内容,“通过”在三种情况下代表不同的事实。
---The v17 prescription set, as of 17.0.0-rc.5
17.0.0-rc.5v17提示集(基于17.0.0-rc.5
)
17.0.0-rc.5This section is a pinned reading, not a live list. It was measured from the
spec sources at the publish and is deliberately bounded so that
entries registered after that publish are a visible delta rather than a silent
contradiction.
17.0.0-rc.5| Reading | Value at |
|---|---|
| |
| Chain support floor | protocol 10 |
| D2 conversions for major 17 | 45 |
| D3 semantic entries for major 17 | 29 |
| 113, across 32 files |
Distinct | 96 |
RETIRED_KEYS_BY_MAJOR[17]data/ExternalFieldMapping:transformintegration/ConnectorFieldMapping:transformshared/FieldMapping:transform
RETIRED_DEFS_BY_MAJOR[17]shared/FieldMappingTransform
Why these two tables are short and the counts above are not. They record retirements registered under the exact-key gates that created them, which are newer than most of protocol 17's work; they are explicitly not a backfill of every retirement ever. The 45 conversions and 113 tombstones are the real size of the v17 surface. Use the tables to answer "was this retirement formally registered", and the conversions/tombstones to answer "what do I have to change" — the second question is the upgrade's question.
本部分是固定快照,而非实时列表。它基于发布时的spec源文件统计,刻意限定范围,以便该版本之后新增的条目可作为可见的增量而非静默矛盾。
17.0.0-rc.5| 统计项 | |
|---|---|
| |
| 转换链支持最低版本 | 协议10 |
| 大版本17的D2转换数量 | 45 |
| 大版本17的D3语义条目数量 | 29 |
已发布 | 113,分布在32个文件中 |
已发布 | 96 |
RETIRED_KEYS_BY_MAJOR[17]data/ExternalFieldMapping:transformintegration/ConnectorFieldMapping:transformshared/FieldMapping:transform
RETIRED_DEFS_BY_MAJOR[17]shared/FieldMappingTransform
为何这两个表格内容少而上述统计数量多。它们记录的是通过精确键校验机制注册的废弃项,这些机制比协议17的大部分工作更新;它们并未回溯所有历史废弃项。45项转换和113个废弃项是v17实际涉及的范围。使用这两个表格回答“该废弃项是否已正式注册”,使用转换/废弃项回答“我需要变更什么”——第二个问题才是升级的核心问题。
How this table is refreshed
如何更新此表格
Never hand-edit the numbers above from memory. Re-measure against whatever
spec the project has installed — one command per row:
bash
undefined绝不要凭记忆手动编辑上述数字。针对项目已安装的spec重新统计——每行对应一条命令:
bash
undefinedprotocol version, support floor, and the per-major conversion / semantic counts
协议版本、支持最低版本、以及每个大版本的转换/语义条目数量
node -e "
const p = require.resolve('@objectstack/spec/package.json');
const j = require(p.replace('package.json','spec-changes.json'));
const e = j.perMajor.find(x => x.to === 17);
console.log(j.protocolVersion, 'floor', j.supportFloor, '| 16→17:',
e.converted.length, 'converted,', e.migrated.length, 'semantic');
"
node -e "
const p = require.resolve('@objectstack/spec/package.json');
const j = require(p.replace('package.json','spec-changes.json'));
const e = j.perMajor.find(x => x.to === 17);
console.log(j.protocolVersion, 'floor', j.supportFloor, '| 16→17:',
e.converted.length, 'converted,', e.migrated.length, 'semantic');
"
tombstone prescriptions actually present in this install
当前安装包中实际存在的废弃提示数量
grep -rho '[REMOVED][^"]*' node_modules/@objectstack/spec/json-schema/ | sort -u | wc -l
If a reading disagrees with the table, **the install wins** — the table is a
snapshot of one publish, and post-`rc.5` registrations are expected to add
entries. Record the delta in the upgrade report rather than editing this
section's pinned numbers; the pin is what makes a later disagreement legible
instead of invisible.
---grep -rho '[REMOVED][^"]*' node_modules/@objectstack/spec/json-schema/ | sort -u | wc -l
如果统计结果与表格不一致,**以安装包为准**——表格是某个发布版本的快照,`rc.5`之后新增的条目是预期的。在升级报告中记录差异,而非编辑本部分的固定数字;固定快照可让后续的差异清晰可见而非被隐藏。
---The v17-canonical shapes, compiled
v17标准格式示例
What the protocol-16 shapes in this skill's examples look like after the
upgrade. This block is type-checked against the published spec, so it cannot rot
into teaching a shape that no longer compiles:
<!-- os:check -->
typescript
import { ObjectSchema } from '@objectstack/spec/data';
import { defineAgent } from '@objectstack/spec/ai';
// `conditionalRequired` → `requiredWhen`; `required` now also states the
// physical constraint explicitly via `storage.notNull`.
export const Lead = ObjectSchema.create({
name: 'crm_lead',
label: 'Lead',
fields: {
name: { type: 'text', required: true, storage: { notNull: true } },
status: { type: 'select', required: true, storage: { notNull: true } },
due_date: { type: 'date', requiredWhen: 'record.stage == "closed"' },
notes: { type: 'textarea' },
},
});
// Agent capability is reached through skills — there is no inline tool list.
export const SupportBot = defineAgent({
name: 'support_bot',
label: 'Support Bot',
role: 'Front-line support triage',
instructions: 'Answer support questions and open cases when needed.',
skills: ['case_management'],
});本技能示例中的协议16格式升级后的样子。此代码块已针对已发布的spec进行类型检查,因此不会过时为无效格式:
<!-- os:check -->
typescript
import { ObjectSchema } from '@objectstack/spec/data';
import { defineAgent } from '@objectstack/spec/ai';
// `conditionalRequired` → `requiredWhen`; `required`现在通过`storage.notNull`明确声明物理约束。
export const Lead = ObjectSchema.create({
name: 'crm_lead',
label: 'Lead',
fields: {
name: { type: 'text', required: true, storage: { notNull: true } },
status: { type: 'select', required: true, storage: { notNull: true } },
due_date: { type: 'date', requiredWhen: 'record.stage == "closed"' },
notes: { type: 'textarea' },
},
});
// 代理能力通过技能调用——不再支持内联工具列表。
export const SupportBot = defineAgent({
name: 'support_bot',
label: 'Support Bot',
role: 'Front-line support triage',
instructions: 'Answer support questions and open cases when needed.',
skills: ['case_management'],
});Failure modes
故障排查
| Symptom | What it actually is | Fix |
|---|---|---|
| Working as designed — the command writes nothing but | Port the printed edits into the sources, then replay from the target major to confirm 0 changes. |
| Replay from the target major still applies changes | The port is incomplete, or a source builds metadata at runtime from a shape the chain never saw. | Diff against |
| An R2 residue item: code reading a renamed key now reads | Exercise the path for real. A green parse says nothing about a |
| A migration-chain-only conversion — no tombstone rejects it, so nothing complains. | Replay the chain anyway. |
| The author-time rule pass, not the schema pass. | Diff against the pre-upgrade |
| A retired key round-trips without error | The schema carrying it is not strict and the key is being stripped, or the key still has a live load-path window. | Determine which — the two need different acceptance evidence. See the reverse check. |
| | Add |
| | Upgrade to the floor by an older route first; the floor is a release-policy boundary, not an oversight. |
| 症状 | 实际原因 | 解决方案 |
|---|---|---|
| 设计如此——命令仅通过 | 将输出的编辑内容移植到源文件,然后从目标大版本重新执行转换链确认无变更。 |
| 从目标大版本重新执行转换链仍有变更 | 移植不完整,或某个源文件通过转换链未检测到的格式在运行时构建元数据。 | 与 |
| R2残留项:代码读取已重命名的键,现在读取 | 实际触发该路径。绿色解析结果无法反映项目代码中的 |
初始 | 仅迁移链执行的转换——无废弃项拒绝旧格式,因此无报错。 | 仍需执行转换链。绿色 |
| 编写时规则校验,而非Schema校验。 | 与升级前的 |
| 废弃键可正常往返 | 对应的Schema非严格模式,键被静默剥离,或该键仍处于加载路径窗口期。 | 确定属于哪种情况——两者需要不同的验收证据。详见反向校验。 |
| | 添加 |
| | 通过旧路径先升级到最低支持版本;最低版本是发布策略边界,而非疏忽。 |
Guardrails (binding)
约束规则(强制性)
- Run the chain first, always. It is the only source of an attributed, schema-proved diff.
- One conversion id per commit, where the project's review culture allows
it. The is the commit's subject line and its justification.
conversionId - No tolerant reads, ever — not in the metadata, not in the project's code. A retirement that gets re-admitted at the consumer is a defect that moved into a repo with no gate over it.
- Ask about intent, decide about mechanics. The split in 2.4 is the contract with the project's owner.
- The report is a deliverable, not a summary. No report, not done.
- Never leave a pending per-deployment data migration unnamed. A gate nobody was told about is served by nobody.
- 始终先执行转换链。它是唯一可追溯来源、经Schema验证的差异来源。
- 每个转换ID对应一次提交(如果项目的审核流程允许)。作为提交主题和依据。
conversionId - 绝不要添加兼容读取逻辑——无论在元数据还是项目代码中。在消费端重新允许废弃项使用,会让缺陷转移到无校验机制的仓库中。
- 意图问题咨询负责人,机制问题自行决策。2.4中的拆分是与项目负责人的约定。
- 报告是交付成果,而非摘要。无报告则不算完成。
- 绝不要遗漏待执行的部署级数据迁移。未告知的校验机制等于不存在。
Cross-skill routing
跨技能路由
- Authoring the corrected metadata — load the domain skill for the shape you are fixing (data, ui, automation, ai, api, i18n).
- Any CEL predicate you rewrite while resolving a residue item — load formula.
- Runtime, plugin, and CLI questions the upgrade turns up — load platform.
- This skill covers the consumption side of a retirement. Designing one inside the ObjectStack platform repo is a different job with its own internal playbook, and it is not this one.
- 编写修正后的元数据——加载对应格式的领域技能(data、ui、automation、ai、api、i18n)。
- 处理残留项时重写的CEL谓词——加载formula技能。
- 升级过程中出现的运行时、插件和CLI问题——加载platform技能。
- 本技能覆盖废弃的消费端处理。在ObjectStack平台仓库中设计废弃逻辑是不同的工作,有其内部操作手册,不属于本技能范畴。