Skip to content

feat(spec,metadata-protocol,service-storage,runtime,cli)!: a seed record can carry a file the package ships, seedFile(path) - #26244

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/project-thread-x6ueoz
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/project-thread-x6ueoz

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #25779

Clause-②: yes (narrowing)
Metadata-protocol: narrowed

维护者速读

改了什么

  • 种子数据现在可以给文件类字段(file / image / avatar / video / audio)填一个应用包里自带的文件,写法是 file: seedFile('fixtures/nda-v1.pdf')。
  • os build 在构建时读出这些文件,把内容打进产物。所以 os dev、os serve、已发布的包、产物 URL 这几种启动方式都带着文件,运行时不用再读磁盘。os validate 做同样的路径检查,找不到文件会在校验阶段就报错。
  • 启动时,种子加载器把文件存进存储服务,把文件 id 写进字段,再把这条记录登记为文件的归属。这和用户上传的文件是同一种归属。每条记录各有一个文件,重启也不会重复存。
  • 新增一条拒绝规则:如果种子在文件字段里写了一个像文件 id、却查不到对应文件的值,这一行会被拒绝并报错。以前这种值会照常写入,读出来却是空文件。

为什么改

  • 维护者在 hotclm#19 裁定 D:平台的种子加载器负责携带应用包里的样例文件,应用里的 file 字段保持必填。
  • 改之前,文件 id 只能通过上传得到,所以带必填文件字段的对象完全无法用种子数据填充。

风险与代价(含回滚)

  • 这是破坏性的收窄。文件字段里查不到文件的 id 形值,以前是静默写入、读出为空,现在整行被拒。
    • 仓库内没有任何种子受影响。唯一带文件字段的示例 app-showcase 没有给文件字段写种子。
    • 外部 URL、内联值,以及没装存储服务的部署,都不受影响。
  • 代价:产物体积大约增加样例文件大小的 4/3(base64 编码)。已发布的种子还会在 sys_metadata 里多存一份。
  • 带种子文件的应用,启动种子改到 kernel:bootstrapped 时执行,和已有的 demoStaff 一样,这样文件会写进部署实际使用的存储。
  • 声明行有偏差:认领写的是 Clause-②: yes 和 Metadata-protocol: added,本 PR 和 changeset 照抄了。实测这是收窄,应写作 yes (narrowing) 和 narrowed,详见下方 Acceptance notes,由席位修正认领。两种写法的发布级别都是 minor,版本号不受影响。
  • 回滚:revert 本 PR 即可,没有数据迁移。已存进存储的种子文件会留着,不受影响。已经用了 seedFile 的应用需要改回去,因为 revert 之后这个函数就不存在了。

席位意见

建议批准。契约复核在本 head 通过(PR 评论 6425043305,CONTRACT_REVIEW_TIER):新增的 seedFile 写法与「查不到文件的 id 形值整行拒绝」两处接受集变化都判定正确,changeset 的收窄声明、BREAKING 说明与 ADR-0087 标记与实际发布内容一致。本仓库没有任何种子受这条收窄影响。唯一未测量的是类型检查,它因 tsc 内存溢出(exit 134,不是类型错误)失败,根因已立卡 #26288,与本 PR 无关,需要重跑。

你要做的

  • 审阅并批准本 PR。它触及 skills/** 的一行生成内容,改动又超过 3000 行(含生成物),所以需要人工批准。

What it does

  • The capability. A seed record can now fill a file / image / avatar / video / audio field with a file the package ships: file: seedFile('fixtures/nda-v1.pdf'). This is the hotclm#19 ruling D: the platform's seed loader carries the bundled fixture file, and the app keeps file required.
  • The shape choice: a reference resolved at build time, with the bytes carried in the compiled artifact. seedFile(path) writes { $file: path } into the record. os build / os compile reads the file, relative to the directory that holds objectstack.config.ts, and embeds it in the dataset's new files map as base64 with its size and SHA-256. Every boot path then has the bytes with no filesystem access at runtime: os dev, os serve, a published package, or an artifact URL.
    • os validate runs the same gate (collectSeedFiles), so it refuses the paths os build refuses: a missing file, a path that climbs out of the package, or an unknown type. Only os build writes the bytes (placeSeedFiles).
    • os serve / os dev booting a config (not an artifact) mirrors the collection, as it already mirrors package docs. Without that mirror a config boot carried the reference and not the bytes, and refused every such row. The boot test's negative control measured exactly that: 0 rows with the mirror removed, 2 with it.
    • The alternative was resolving the path at load time against the package directory. It was rejected because a published package or an artifact URL has no package directory.
    • The cost: the artifact grows by about 4/3 of the fixture bytes. A published seed also keeps those bytes in sys_metadata.
  • What the loader does with it.
    • It stores the bytes through a new field-files service (IFieldFileService, registered by @objectstack/service-storage) as a pending sys_file in the row's organization.
    • It writes the id into the field and, once the row lands, makes the (object, record, field) slot the file's owner. That ownership is the same one an upload gets, and it commits the file.
    • Each row gets its own file.
    • It is idempotent across boots. When a replay finds the row already holding a file stored from the same path with the same bytes, name and type, it keeps that file.
    • A file whose row is refused stays pending, so the existing pending-upload retention reclaims it.
  • The narrowing (shape 3), refused rather than warned. A seed value in a file field that looks like an id but names no stored file in the row's organization is now refused for that row. Before this change it was written, and the field then read as holding no file. URLs and inline values are untouched, and so is a deployment without the storage service.
    • Refuse versus warn is not a real fork; all four axes agree:
      • Business need: a seed that silently produces "no file" in a required field is worse than a loud row error.
      • Long-term design: ADR-0104 already classes such a value as a blocking unowned_reference.
      • Preventing AI mistakes: a fabricated "id that looks right" is exactly the pattern this catches. examples/app-showcase's seed docblock names it.
      • Keeping scope small at this stage: refuse adds no warn channel and no switch.
    • No seed in this repository is refused. The only example with file fields, app-showcase, seeds none of them.

Readiness: why the inline seed of an app with seed files moves to kernel:bootstrapped

  • The field-files service is registered at the storage plugin's init and resolves the engine per call, so it can serve a seed that runs in a plugin's start().
  • The storage adapter can be swapped at kernel:ready, once settings are read. An AppPlugin inline seed whose datasets carry seed files therefore defers to kernel:bootstrapped, as one with demoStaff already does, so its files land in the store the deployment serves from.
  • When the field-reference hooks are armed, they claim the file on the record write, and claimFiles then only commits it. When they are not armed yet, the loader's explicit claim is the ownership. Either way a file has exactly one owner.
  • Residual edge: a multi-tenant organization created before the settings read seeds through the constructor's adapter. That is the same as an upload in that window, and this PR does not change it.

Files touched

Area Files
Spec: authoring packages/spec/src/data/seed-file.zod.ts (new: seedFile, SeedFileReferenceSchema, SeedFileContentSchema, SeedFilePathSchema), seed.zod.ts (files map, record-value refinement, defineSeed refuses seedFile in a non-file field), data/index.ts
Spec: contract packages/spec/src/contracts/storage-service.ts (FIELD_FILE_SERVICE, IFieldFileService, its input and output types)
Storage packages/services/service-storage/src/field-file-service.ts (new), storage-service-plugin.ts (registers field-files), file-reference-lifecycle.ts (only export added to releaseOwnership and ownedBySlot)
Loader packages/metadata-protocol/src/seed-files.ts (new), seed-loader.ts, protocol.ts, protocol-publish-batch.ts, index.ts
Runtime packages/runtime/src/app-plugin.ts, packages/runtime/src/domains/packages.ts (pass the field-files service to every SeedLoaderService)
CLI packages/cli/src/utils/bundle-seed-files.ts (new: collectSeedFiles gate, placeSeedFiles assembly), commands/compile.ts, commands/validate.ts, commands/serve.ts (config-boot mirror); the gate-parity and call-site ledgers in test/validate-build-gate-parity.test.ts and test/normalized-call-sites.test.ts classify both
Docs content/docs/data-modeling/seed-data.mdx (new section "File Fields — seedFile(path)")
Generated / ledgers spec authorable surface, JSON-schema manifest, api-surface, export origins, declaration map, references docs (new data/seed-file page), skill references, strictness-ledger counts plus a hand-written triage row for seed-file.zod.ts (both sites strict from birth), dropped-refinements.baseline.json (the record-value refinement, by site), liveness seed.json + counts

Governed path. The diff touches one governed path, skills/objectstack-data/references/_index.md. It is a single generated line from gen:skill-refs (the new data/seed-file.zod.ts reference), and check:skill-refs regenerates it byte-exact. The two skills/** readings, in lines:

  • The changed file: 73 before, 74 after.
  • The whole package (every SKILL.md summed): 4383 before, 4383 after. No SKILL.md changes.

Overlap with #25801 (PR #26134). That PR opened first, and the two diffs share no file. This PR's only edit near #25801's area is packages/services/service-storage/src/file-reference-lifecycle.ts, where it adds the export keyword to two existing functions and changes no logic.

Tests

  • packages/spec/src/data/seed-file.zod.test.ts: the path rule's accept and refuse tables, the reference shape, SeedSchema with refs and bytes, the malformed-reference path, and defineSeed's refusal.
  • packages/spec/src/contracts/storage-service.test.ts: the field-files slot and a fake round trip.
  • packages/services/service-storage/src/field-file-service.test.ts: real ObjectQL on sqlite. It covers:
    • store before kernel:ready;
    • the 413 size refusal;
    • validation;
    • organization-scoped describe under the isolated posture;
    • claim, idempotence and release;
    • 409 and 404;
    • agreement with the reference hooks;
    • a multiple field.
  • packages/metadata-protocol/src/seed-loader-seed-files.test.ts. It covers:
    • first load, replay keeping the file, and changed bytes storing a new file;
    • multiple fields, a file per row, and the self-reference path;
    • every by-row refusal: missing bytes, a SHA mismatch, no service, a mixed field, several references in a one-file field, shape 3 (including per organization), and a claim failure;
    • dry run, and an object with no file fields.
    • A refused row stores nothing.
  • packages/cli/src/utils/bundle-seed-files.test.ts: bundling, package bodies, the gate writing nothing, a declared type winning over the extension, and refusals (missing file, unknown type, a path that climbs out).
  • packages/cli/test/build-seed-files-artifact.e2e.test.ts (nightly tier): the real os build writes the bytes beside the reference, loadArtifactBundle reads them back, and both os build and os validate refuse a path that names no file.
  • packages/cli/test/serve-config-boot-seed-files.e2e.test.ts (nightly tier) is the card's acceptance, end to end. os serve --dev boots a project whose clm_version.file is required and seeds two versions from one fixture. It then checks:
    • each row holds a file of its own;
    • following GET /storage/files/:fileId returns the fixture's bytes, so the file is stored, committed and owned.
    • Negative control: with the serve.ts mirror removed, the same boot seeds 0 rows.

Local verification

The head is 2d0dd7dd, which merges cleanly with current main. It differs from b8fe94f8 only by the changeset's two declaration lines, with no code change.

  • Derived gates, on b8fe94f8.
    • dispatch-gates --commands derives 140 commands for this diff, and 139 of them exit 0.
    • check:dual-build-cjs-loads exits 3 because it needs a full workspace build, so it is left to CI.
    • dispatch-gates --ran reconciles all 140, with 0 unrun.
    • The four changeset gates ran again on 2d0dd7dd and exit 0: check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main and check:changeset-gate-self-tests.
  • Dogfood, on b8fe94f8 (packages/qa/dogfood): 296 files passed and 3 skipped; 2523 tests passed and 18 skipped.
  • Full package suites, on the final code:
    • @objectstack/metadata-protocol: 267 files; 21554 tests passed and 19 skipped.
    • @objectstack/service-storage: 66 files; 1206 tests passed.
    • @objectstack/runtime: 457 files; 8352 tests passed and 19 skipped.
    • @objectstack/cli: 506 files, 0 failed tests.
      • 491 files ran on a7fc798d. The commits after it touch no CLI file, and their only source changes are type-level: an annotation in the storage plugin and type aliases in seed-file.zod.ts.
      • The 15 files that need a built CLI ran on b8fe94f8.
  • Rerun directly on the final code:
    • the two nightly-tier suites, with OS_TEST_TIERS=nightly: 3 tests pass;
    • the CLI unit suites for the gate and its two ledgers: 46 tests pass;
    • the spec suites (the alias pin at 790, seed-file.zod, the storage contract): 51 tests pass.

The full gate list with exit codes is in the dev report on #25779.

Acceptance notes

  • The declaration lines differ from what was measured. The two lines at the top of this body, and the same two in .changeset/25779-seed-files.md, are copied verbatim from the Claim.
    • Measured, the change narrows: shape 3 refuses a seed value that used to be written.
    • The rule in pr-automation.yml says to name the kind highest in breaking-ness. By that rule the lines should read Clause-②: yes (narrowing) and Metadata-protocol: narrowed.
    • Both kinds grade minor, so the release level does not change.
    • The changeset carries the BREAKING banner, the ADR-0087 disposition marker (not-required (no-migration-prescription)) and a "What to do" line either way.
    • The correction: amend the Claim, then change both lines here and in the changeset.
  • Scope beyond the Claim's file surface. Each item was needed for the card's acceptance:
    • os validate and the os serve config-boot mirror;
    • the CLI's gate-parity and call-site ledgers;
    • the packages/runtime callers that hand the loader the field-files service;
    • the contract in packages/spec/src/contracts/storage-service.ts.
  • The landing point is the metadata-protocol loader, as the Claim's premise measured. packages/runtime/src/seed-loader.ts is not touched.
  • Out-of-scope findings: none.

🤖 Generated with Claude Code

https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF

claude added 10 commits October 26, 2026 12:25
… rows store nothing

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
…ctness-ledger row for seed-file.zod.ts

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
…ap for the seed file type aliases

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/service-storage, @objectstack/spec, touching 99 documentable anchor(s). ⚠️ 14 changed file(s) yielded no anchor (packages/metadata-protocol/src/index.ts, packages/runtime/src/domains/packages.ts, packages/spec/api-surface/contracts.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

73 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6390fe52cecd263f09225d0d6d9b74412d111a67.

⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 14 changed file(s) yielded no anchor (packages/metadata-protocol/src/index.ts, packages/runtime/src/domains/packages.ts, packages/spec/api-surface/contracts.json, …) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: recordId (4 routes)
  • 3 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 38 pages), os serve (command, 41 pages), os validate (command, 75 pages)
  • 23 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 158 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6390fe52cecd263f09225d0d6d9b74412d111a67 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4f2c6a2f9b431afdf4088ddca718bf4c8feeb535 — the merge of head 8f07f5cb1cd03b0fc37c03c192fb7b081a38f3ab into base 6390fe52cecd263f09225d0d6d9b74412d111a67, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4f2c6a2f9b431afdf4088ddca718bf4c8feeb535 && git checkout 4f2c6a2f9b431afdf4088ddca718bf4c8feeb535
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6390fe52cecd263f09225d0d6d9b74412d111a67 8f07f5cb1cd03b0fc37c03c192fb7b081a38f3ab && git checkout -B drift-repro 6390fe52cecd263f09225d0d6d9b74412d111a67 && git merge --no-ff 8f07f5cb1cd03b0fc37c03c192fb7b081a38f3ab

node scripts/docs-audit/affected-docs.mjs --json 6390fe52cecd263f09225d0d6d9b74412d111a67

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6390fe52cecd263f09225d0d6d9b74412d111a67 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Regenerated the deferred spec artifacts from the merged tree: the strictness-ledger counts, api-surface, authorable-surface and export-origins for data.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
…test record names no error code

The ledger gained two entries and 26 dropped-refinement sites (the seed record value's refinement) without its measured totals; they now read 247 and 866. A bundle test's record field named code read as an error-code literal to the casing guard; it is a name.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
…l-1 description pin counts seed-file.zod.ts

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c33b95d89fa4a66dd871a0ab617f49bf870421e9
Local-runs: none
Scope: packages/spec/src/contracts/storage-service.ts, packages/spec/src/data/index.ts, packages/spec/src/data/seed-file.zod.ts, packages/spec/src/data/seed.zod.ts, skills/objectstack-data/references/_index.md + packages/metadata-protocol/src/seed-files.ts, packages/metadata-protocol/src/seed-loader.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-publish-batch.ts, packages/metadata-protocol/src/index.ts, packages/services/service-storage/src/field-file-service.ts, packages/services/service-storage/src/storage-service-plugin.ts, packages/services/service-storage/src/file-reference-lifecycle.ts, packages/cli/src/utils/bundle-seed-files.ts, packages/cli/src/commands/compile.ts, packages/cli/src/commands/validate.ts, packages/cli/src/commands/serve.ts, packages/cli/test/validate-build-gate-parity.test.ts, packages/cli/test/normalized-call-sites.test.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/domains/packages.ts, packages/spec/api-surface/{contracts,data}.json, packages/spec/authorable-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/json-schema.manifest/data.json, packages/spec/liveness/seed.json, content/docs/data-modeling/seed-data.mdx, .changeset/25779-seed-files.md. The rest of the diff (tests, generated references, ledgers, llms.txt, engine-double pin) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs: card #25779 (body + all 7 comments, including the claim 6415670720, the claim amendment 6421695515 and both dev reports), PR #26244 (body, 55-file list, net diff against main at the head), and the 48 check-runs on the head. Read-only: nothing built, run or re-run. Rendered 2026-10-26T19:12Z.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged against the cited code:

  1. @objectstack/spec/data widens — 11 new exports (seedFile, isSeedFileReference, isSeedFilePath, SeedFilePathSchema/SeedFilePath, SeedFileReferenceSchema/SeedFileReference/SeedFileReferenceParsed, SeedFileContentSchema/SeedFileContent/SeedFileContentParsed) via data/index.ts; api-surface/data.json lists exactly those 11. Right.
  2. Seed schema widens — records values may now be { $file, name?, contentType? } (alone or as elements of an array); a new optional files map keyed by SeedFilePathSchema. authorable-surface/data.json adds data/Seed:files plus the 3 + 4 child keys; json-schema.manifest adds the three schemas; declaration-map adds the three pairs. files is authorable (the loader's refusal text names the hand-written-dataset path), consistent with the schema. Right.
  3. Seed.records narrows at parse — SeedRecordValueSchema refuses an object that CLAIMS $file but is malformed (unknown key, a path with a ./../empty segment, leading /, drive letter or \), where z.unknown() used to accept it. The path regex refuses every case the changeset lists and accepts .hidden/x and ..foo correctly. The refinement is dropped from JSON Schema and registered in dropped-refinements.baseline.json. Right (a value that had no meaning before; it is still a narrowing and the changeset says so).
  4. defineSeed narrows at call time — assertSeedFilesOnFileFields throws for seedFile(...) on a field whose type is not in FILE_REFERENCE_TYPES (image, file, avatar, video, audio, on main). A field the object does not declare is left to assertSeedKeysDeclared. Right.
  5. @objectstack/spec/contracts widens — FIELD_FILE_SERVICE = 'field-files', IFieldFileService (storeFile/describeFiles/claimFiles), StoreFieldFileInput, StoredFieldFile, FieldFileSlot, FieldFileDescription; api-surface/contracts.json and export-origins/contracts.json carry the 6. The implementation in field-file-service.ts honours the documented envelope: 400 VALIDATION_ERROR, 413 via uploadSizeRefusal before any write, 404 RECORD_NOT_FOUND, 409 RESOURCE_CONFLICT (never a transfer), 503 SERVICE_UNAVAILABLE; write order is row-then-bytes with the pending row deleted on a failed upload; claimFiles releases the slot's other files through the lifecycle's own releaseOwnership (only export added to it and ownedBySlot). Registered in storage-service-plugin.ts init with the engine read per call, so it serves a seed that runs in a plugin's start(). providesServices gains field-files. Right.
  6. Seed-load accept set narrows (shape 3, the card's ask ✨ Set up Copilot instructions #2) — seed-files.ts#prepareRow: when a file-class field holds no $file reference and the load has a field-files service, every element matching isFileIdToken (/^[A-Za-z0-9_-]{1,64}$/, on main) must resolve through describeFiles in the row's organization, else the row is refused before any write, counted in errored/allErrors and logged at error. URLs, inline values and a composition without the service are written as before. The organization is read off record['organization_id'], which the loader stamps at seed-loader.ts:1513 on main, above the hunk anchor (1867), so it is final when judged. The changeset's BREAKING paragraph describes exactly this set. Right.
  7. Seed-load accept set widens (the card's ask Add metamodel interfaces for ObjectQL/ObjectUI contract #1) — a $file reference is checked against the dataset's files entry (present, size, sha256), stored as a pending sys_file with provenance { seedFile, sha256 }, the id written into the field (array for multiple), and the slot claimed after the row lands on every write site (insert, batched insert/update, skipped replay). Idempotent: a held file with the same path, digest, name and type and the same (or no) owner is kept; each held file serves once, so 300 rows naming one path get 300 files. Refusals by row: missing bytes, digest mismatch, no service, a mixed field, several refs in a one-file field, a store failure. A refused row stores nothing (every field is judged before any storeFile). Replay reads use RAW_FILE_VALUES_CONTEXT_KEY so held ids are compared in stored form. Right, and it matches the ruling hotclm#19 D (loader carries the fixture; file stays required).
  8. SeedLoaderService constructor widens — optional 4th argument { getFieldFiles }, exported as SeedLoaderServiceOptions; all five platform callers pass it (app-plugin.ts x3, domains/packages.ts, protocol.ts → applySeedBodies). Absent, a seed-file row is refused by name and shape 3 is not judged. Right.
  9. Inline boot seed readiness narrows — an AppPlugin inline seed whose datasets carry files defers to kernel:bootstrapped, as the demoStaff branch already does, so files land in the adapter the storage settings select at kernel:ready. Right.
  10. CLI — collectSeedFiles is the gate (os validate step 3d and os build step 4a refuse the same three cases: climbs out of the config's directory, names no file, type untellable and undeclared), placeSeedFiles the assembly (os build only). os serve/os dev on a config mirrors collection and only warns; the loader then refuses the row. The parity ledger classifies collectSeedFiles as a shared non-registry gate and placeSeedFiles as assembly; the call-site ledger classifies the stack.data read. Right.
  11. Governed line — skills/objectstack-data/references/_index.md +1 line, the gen:skill-refs row for the new module; check:skill-refs runs in the green Lint lanes, so it is byte-exact regeneration. It stays a Tier H path per the claim amendment. Right.
  12. Docs — seed-data.mdx "File Fields" section states path rule, build/validate/serve behaviour, per-row files, replay keep and the three refusals; each sentence matches the cited code. Right.

② Semver level

  • Changeset .changeset/25779-seed-files.md at the head: @objectstack/spec, @objectstack/metadata-protocol, @objectstack/service-storage, @objectstack/runtime, @objectstack/cli all minor; these are exactly the five published packages the diff moves. Clause-②: yes (narrowing) and Metadata-protocol: narrowed: the diff both adds ($file, files) and narrows (shape 3 refusal; malformed $file refused at parse), and narrowed wins over added. (narrowing) is BREAKING: the title carries !, the body carries the BREAKING banner, the FROM → TO fix ("replace the value with seedFile(path) or remove it") and exactly one ADR-0087 marker, not-required (no-migration-prescription), which is correct — no authorable key is removed, renamed or re-shaped, so there is no tombstone to ship. A kind takes at least minor; no package takes major. Matches what the diff publishes. Check Changeset is green on the head.
  • The Clause-②: line on the changeset matches the amended claim (6421695515). The PR body still reads Clause-②: yes / Metadata-protocol: added — see ③.
  • Not skip-changeset: correct, every one of the five packages publishes.

③ Boundary flags

  • Dev flag: declaration lines. Measured narrowing, claim said yes/added. Answered: the claim was amended (6421695515) and the changeset now carries yes (narrowing) / narrowed (6c5c49c). The PR body still carries the claim's original yes / added and is out of step with the changeset it ships; the seat has said it will patch the body after this review. This record judges the changeset; the body mismatch is noted and left to the seat.
  • Dev flag: file surface beyond the claim (os validate, the os serve config-boot mirror, the two CLI ledgers, the runtime callers, the contract file). Judged in ① items 5, 8 and 10: each is required for the card's acceptance and none widens past the ruling. Answered.
  • Dev flag: gate-driven additions (strictness-ledger row, ADR-0122 Parsed aliases and the alias pin 789 → 790, engine-double pin regenerated, dropped-refinements baseline, level-1 description pin 30 → 31). Mechanical consequences of a new *.zod.ts module; the gates that read them are green. Answered.
  • Dev flag: dogfood and the full sweep ran on b8fe94f8, not the head. The head's CI check-runs cover the final tree (Test Core 8/8, Dogfood Regression Gate, Spec property liveness, all Lint lanes, Build Core, Build & E2E UI, Type Check UI, Type Check · consumer/source/debt all success). Answered by the check-runs.
  • Not in the check-runs: the two end-to-end suites that are the card's acceptance (build-seed-files-artifact.e2e.test.ts, serve-config-boot-seed-files.e2e.test.ts) are nightly-tier and did not run on this head in CI. The dev reports 3/3 passing locally with the negative control (0 rows without the serve mirror, 2 with). Noted for the maintainer; not a FAIL ground, since every PR-tier gate on the head is green and the unit suites (loader 16, storage 9, CLI 8 + ledgers, spec 51) cover each claimed behaviour by name.
  • open_questions (report 6424852503, one entry): the tsc heap OOM on Type Check · workspace (job 121738515222) and its roll-up TypeScript Type Check (121747810706), exit 134 in packages/plugins/plugin-hono-server, no type error, same lane green on 6c5c49c with identical types. Escalated: it is filed as finding(tooling): CI "Type Check · workspace" OOMs in plugin-hono-server (tsc exit 134, no type error) because its tsconfig type-checks spec's dist .d.ts without skipLibCheck, and the heap margin on main is nearly gone #26288 and is not this PR's defect. These two check-runs are NOT MEASURED for this record — they are not a type verdict either way. Option A (re-run once) is the seat's call; option B (skipLibCheck in plugin-hono-server) would widen this PR past the card and belongs in its own PR. The PR's own typing is otherwise measured by the three green Type Check sub-lanes, Build Core and Test Core.
  • out_of_scope_findings: the OOM margin finding (class a) is carried by finding(tooling): CI "Type Check · workspace" OOMs in plugin-hono-server (tsc exit 134, no type error) because its tsconfig type-checks spec's dist .d.ts without skipLibCheck, and the heap margin on main is nearly gone #26288. Nothing else outstanding.
  • Standing on the PR: one review exists, APPROVED by os-zhuang on 6c5c49c (not dismissed). Whether that is the Tier H GOVERNED_APPROVERS word is the queue guard's reading and the maintainer's, not this record's; this record is the contract-tier review the route owes, and Tier H landing still waits on the maintainer per Prime Directive 14.
  • Gate tally on c33b95d8: 48 check-runs — 39 success, 7 skipped (opt-in/UI matrix lanes), 2 failure (the NOT MEASURED pair above). Governed Surface Queue Guard, Check Changeset, Check PR Size, Lint & Repo Gates and every scoped Lint lane are green.

Implemented-by: claude/project-thread-x6ueoz
Reviewed-by: contract-review-26244-fable — a subagent of seat session_01HbmCEPMBmMLU3VZCEzfec7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 — PR #26244 at c33b95d89fa4a66dd871a0ab617f49bf870421e9

改了什么:种子数据可以给文件类字段填写应用包自带的文件,写法是 seedFile('fixtures/x.pdf')。os build 构建时把文件打进产物;启动时,种子加载器把文件存进存储服务,并把记录登记为文件的归属。新增一条拒绝规则:文件字段里如果写了一个像文件 id 的值,却查不到对应的文件,这一行会被拒绝。

为什么改:执行维护者在 hotclm#19 的裁定 D。改之前,带必填文件字段的对象无法用种子数据填充。

风险与代价(含回滚):这是破坏性收窄(Clause-②: yes (narrowing) / Metadata-protocol: narrowed,版本级别 minor),本仓库没有种子受影响。产物会增大,增量约为样例文件大小的 4/3。回滚直接 revert 本 PR,不涉及数据迁移。

席位意见:建议合入。契约复核 6425043305 判 PASS,席位核对见下文,全部通过。

你要做的:只有一件事,就是对 Lint & Type Check 运行 40370187707 点「Re-run failed jobs」。这一项红是 tsc 内存溢出,见 #26288。os-zhuang 的批准(在 6c5c49cc 上)已经在案。重跑转绿后,由席位转为 ready 并经合并队列落地。

席位核对

Seat domain:spec#4, session session_01HbmCEPMBmMLU3VZCEzfec7, 2026-10-26T19:14Z.

  1. Shape. The PR is a draft against base main, and its first line is Fixes #25779, which is right because the whole card is delivered. The title carries !. The body's declaration lines were patched to match the changeset and amendment 6421695515.
  2. Premise, measured. Ruling D of hotclm#19 is implemented as written. The only in-repo producer with file fields, app-showcase, seeds none, so the narrowing reaches no producer.
  3. Gates and check-runs. At c33b95d8, 39 check-runs succeeded and 7 were skipped. Type Check · workspace and its roll-up TypeScript Type Check are NOT MEASURED: tsc ran out of heap in plugin-hono-server (exit 134, no type error), which is finding(tooling): CI "Type Check · workspace" OOMs in plugin-hono-server (tsc exit 134, no type error) because its tsconfig type-checks spec's dist .d.ts without skipLibCheck, and the heap margin on main is nearly gone #26288's class. The relay cannot re-run that workflow, because it is not ci.yml. The dev's local results at this head: dispatch-gates 139 of 140, with dual-build-cjs-loads left to CI; dogfood 297 of 300 files (VERDICT 0); spec tests 21,995 passed.
  4. Accepted deviations. The declaration lines are corrected in place of the claim's lines. The two nightly-tier e2e acceptance suites did not run in CI. The dev ran them 3/3 locally, with a negative control.
  5. Out of scope. The typecheck OOM margin is filed as finding(tooling): CI "Type Check · workspace" OOMs in plugin-hono-server (tsc exit 134, no type error) because its tsconfig type-checks spec's dist .d.ts without skipLibCheck, and the heap margin on main is nearly gone #26288. No other findings.
  6. Landing. This is Tier H: a governed skills/** generated line, which the [finding] skills/*/references/_index.md is generator-owned (gen:skill-refs) yet sits inside the skills/** governed-merge fence — every spec PR touching an indexed headline is forced to carry a governed-surface file #11705 exception did not lift, and 3,402 changed lines. GOVERNED_APPROVERS approval os-zhuang APPROVED stands on 6c5c49cc. Once the type-check lane is green, the owning seat flips the PR ready and arms auto-merge. A merge by the maintainer is the other way to land it.

Takes main's plugin-hono-server skipLibCheck fix. Two conflicts, resolved by keeping both sides:
- metadata-protocol protocol.ts imports: main dropped the unused ALL_CONVERSIONS import; this branch keeps its IFieldFileService and FIELD_FILE_SERVICE imports.
- the ADR-0122 isomorphic pin count: main's NowAnchorSchema pin is 789 -> 790, so this branch's SeedFilePathSchema pin becomes 790 -> 791 (assertion, title and header).

Regenerated the deferred spec artifacts from the merged tree: json-schema manifest, authorable-surface, export-origins, declaration-map and api-surface for data, the strictness-ledger counts and the references index.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eJQ8dtut5bZsnhFE3XGHF
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 01:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 01:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 2163e80 Oct 27, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-x6ueoz branch October 27, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

3 participants