Skip to content

feat(rest): 导出接口新增 ?template=true —— 输出只含「可填列」的 xlsx 导入模板 #18386

Description

@baozhoutao

Ruled: 5904855243 · letter Q1 A · Q2 C — ISecurityService.getWritableFields is added and the template narrows by it (round 2 on PR #20683, Part of #18386: acceptance 6 is a hand check in four spreadsheet apps, so the card stays open for it after the PR lands; ruling 5906743185); the column rule keeps system and drops hidden; acceptance 1 is corrected accordingly · 2026-09-30T05:41Z

问题

导入模板目前没有统一实现,两个仓库各做了一套,且都把用户填了也写不进去的列放进了模板。

objectui 的「下载模板」按钮前端自行拼 CSV,列取自 importTargetFields 的全量结果;framework 这边 GET /data/:object/export 的默认列是 buildFieldMetaMap(schema) 的全部 key,只按 FLS 收窄,不看 system / hidden / readonly。注释里 rest-server.ts 明确写了「an empty export doubles as an import template」——但空导出只保证「没有数据行」,没回答「表头该有哪些列」。

实测一个业务对象(项目),模板前 7 列是 registry 注入的系统字段:

列 字段 readonly hidden system
组织标识 organization_id ✅ ✅ ✅
创建时间 created_at ✅ — ✅
创建人 created_by ✅ — ✅
更新时间 updated_at ✅ — ✅
更新人 updated_by ✅ — ✅
所有者 owner_id ❌ — ✅
所属业务单元 owning_business_unit_id ✅ ✅ ✅

因为是 registry 注入,每个对象都这样,不是个别对象配错。

比「列太多」更严重的是:这些 readonly 列会被 stripReadonlyFields 在写入时静默剥掉(engine.ts)。用户照模板填了「创建人」,导入成功、无报错、数据没进去。模板在教用户填注定被丢弃的列。

为什么不能直接改导出的默认列

导出和模板的诉求相反:

所有者 / 修改时间
导出(我要看数据) 要 —— 列表上显示了,导出就该有
模板(我要填数据) 不要 —— 填了会被平台盖掉或 strip

同一套列规则无法同时满足。所以导出的列规则保持不变,模板作为新模式引入 —— 这样对现网调用方零影响。

方案:一个接口,两个模式

GET /data/:object/export                 → 现状,不改
GET /data/:object/export?template=true   → 新增

目标列规则

导出模式(不改) 模板模式(新增)
起点 schema 全字段 schema 全字段
system 保留 排除
hidden 保留 保留(可写即进模板,裁定 5904855243 Q2 C)
readonly 保留 排除
formula / summary 保留 排除
autonumber 保留 排除
FLS ∩ 可读 ∩ 可写
?fields= 显式指定 照办,不收窄 照办,不收窄
数据行 有 0 行
列顺序 作者声明顺序 作者声明顺序(不重排,必填靠 * 标记)

判据一句话:「这列我填了,导入后真的会落库吗?」答"会"才进模板。 每条排除都对应引擎里一个真实的丢弃或拒绝行为,不是审美判断。

输出格式:xlsx(不是 CSV)

模板必须表达值域,CSV 做不到:

CSV xlsx
封闭值域下拉 做不到 Excel 数据验证,点选不会错
multiselect 用逗号分隔 与 CSV 分隔符打架 无冲突
值域说明 只能污染表头 独立 sheet
前导零(00123) 被 Excel 吃成 123 可声明文本单元格

具体结构:

  • Sheet1「模板」:表头(必填带 *)+ 一行示例值;select / radio / boolean 列挂数据验证下拉
  • Sheet2「填写说明」:每字段一行 —— 合法值、格式、分隔符;lookup 写「填 <目标对象> 的名称」。同时兼作下拉数据源(Excel 内联下拉有 255 字符上限,选项多时必须引用区域)

值域怎么写

导入端(import-coerce.ts)实际接受的范围比模板告诉用户的宽得多,模板应如实传达:

类型 导入端接受 模板该给
select / radio ① value 精确 ② label 大小写不敏感 ③ 翻译后 label label(用户看得懂,导入端认)
multiselect 按 , ; 、 换行 拆分 示例用 、 连两个真实选项;说明列出四种分隔符
boolean true/t/yes/y/1/on/是/对/✓/√ + 反面 示例给「是」,不是 true
lookup / reference 显示名 → resolveRef 解析 说明写「填名称」,提示重名会 reference_ambiguous
number 容忍 1,234 / $¥€£¥ / 25% / (100) 说明里写明

实现要点

已定的边界

  • upsert 匹配列:模板不预置 autonumber。要 upsert 的人自己加列,映射步骤里照样选得到 —— 功能不丢。理由:为少数场景在模板里塞一列「填了新建时无效」的东西,又回到"教人填没用的列"的老问题。
  • 示例值行:保留。它传达格式,Sheet2 说明里提示用户删除。

验收

  1. 任一业务对象 ?template=true,下载的 xlsx 中不含任何 system / readonly / formula / summary / autonumber 列;作者声明 hidden: true 且可写入的字段保留(裁定 5904855243 Q2 C)
  2. 保留列的顺序与作者 fields 声明顺序一致;必填且没有默认值的列,表头带 *(裁定 5896083518 Q3 A)
  3. select / boolean 列在 Excel 中可下拉选择
  4. 不带 ?template=true 的导出,列与本 issue 之前逐字节一致(现网零行为变更)
  5. 下载的模板填入数据后原样导回,不产生 invalid_option / invalid_boolean
  6. 跨表格软件实测:Excel / WPS / Numbers / Google Sheets 下拉均正常 —— 单测证明不了,需真下载真打开

关联

objectui 侧改动(「下载模板」改调本接口、删除前端 CSV 生成)见 objectstack-ai/objectui 的对应 issue。本 issue 落地后 objectui 那侧才能动。

Activity

  1. self-assigned this
    on Sep 16, 2026
  2. baozhoutao commented on Sep 16, 2026

    @baozhoutao
    ContributorAuthor

    objectui 侧对口 issue:objectstack-ai/objectui#9600(「下载模板」改调本接口、删除前端 CSV 生成)。该 issue 依赖本 issue 落地并发版后才能开工。

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Status check from the triage seat, on the maintainer's instruction. 2026-09-28T09:34Z.

    Provenance. Executed on the maintainer's instruction. In the triage seat's chat (session session_01AavokzJ5DndAwitDXvKy4U, 2026-09-28), the seat's owned-card review listed this card under item 5 (a reminder on each card held by baozhoutao), and the maintainer replied, verbatim: 「v18 还没开始。其他同意,长期项目: 具体列出来按照总监决策的格式和我讨论」.

    @baozhoutao: this card is assigned to you. It has no labels, and has not moved since 2026-09-16. Its objectui pair, objectstack-ai/objectui#9600, waits on it.

    • Still yours: a line on the next step keeps it. Triage then grades and routes it with you as the owner.
    • Not being worked: clear the assignee, and triage grades and routes it for dispatch.
  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Pointer from domain:cli (#6024): the import reader's number rule changes, and this card's 1,234 row should follow it

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-28T21:42Z · ⛔ not a claim; this card stays with its assignee

    PR #20517 (#20497) makes /import's number reader accept a comma only in a well-formed thousands group: 1 to 3 leading digits, then groups of exactly 3, and only before any .. Examples: 1,000, 12,345.67, (1,234). Every other comma is refused as the row's invalid_number, and a decimal comma such as 3,14 is refused, never guessed. This card's value-domain row for number lists 1,234 among the tolerated forms. That form stays admitted; the row should add the rule and the refusal, in the wording of that PR's changeset (.changeset/20497-import-number-thousands-group.md).

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 10 · takeover on the maintainer's instruction · 2026-09-29T15:27Z
    Session: session_01Sfe5YjBLwB9J3y8fvm2xq1
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-18386-export-import-template
    Worktree: objectstack-issue-18386
    Seat: domain:spec#5 (seat post #19357)
    Why this seat: the maintainer wrote in this seat's session, verbatim: 「#18386、#12438: 如果没有人在处理,你直接接手」. Read at this stamp:

    • No open or merged PR carries this card's number, and no remote branch names it.
    • packages/rest/src has no template query mode.
    • The card has not moved since 2026-09-16. The triage status check on 2026-09-28 has no answer.
      So baozhoutao comes off as assignee in this act. The objectui half, fix(plugin-grid): 「下载模板」列出全部字段(含系统/只读列),改调后端模板接口 objectui#9600, stays with its assignee.
      File surface (stop on breach; explain in the report):
    • packages/rest/src/rest-server.ts: the /data/:object/export route and DATA_EXPORT_PARAMS.
    • New files under packages/rest/src/ for the template column rule and the workbook, with their tests.
    • Every test or pin that counts or enumerates what the change moves, starting with rest-server-closed-query-params.test.ts. The dev names each one.
    • .changeset/18386-*.md.
      Container & model: M, mode:subagent, model: opus. dispatch-gates --tier at 89801cd963: no path-derived mandate. check-governed-merges --test: not governed.
      Clause-②: yes (the seat's reading: the export door accepts one more query parameter and a new response shape, so the public surface widens; the at-tier review is owed before enqueue)
      Thread-read: 5879289195
      Triage gap: the card carries no Path: line, grade or domain:* label. It is taken on the maintainer's word, not from the take order.

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 18386,
    "status": "blocked",
    "branch": "claude/issue-18386-export-import-template",
    "pr": "#20683",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1",
    "premise_still_valid": true,
    "summary": "Implemented in draft PR 20683 (head 21aea1a, base 6bff748): GET /data/:object/export?template=true answers an xlsx import template. A new module packages/rest/src/import-template.ts holds the column rule (one exclusion per card row: system, hidden, readonly, formula/summary, autonumber), the reading of ?template= (true/false only; row params, header and a non-xlsx format are refused 400 VALIDATION_ERROR), a Template sheet (header with ' ' on a required field that has no default, one example row, list dropdowns for select/radio/boolean over rows 2..50001 sourced from ranges on the Instructions sheet) and an Instructions sheet whose accepted-value text is BUILT from TEMPLATE_READER_CLAIMS and the spec boolean tokens, all run through import-coerce.ts's parsers in tests. rest-server.ts adds 'template' to DATA_EXPORT_PARAMS and to the multiplicity list and branches to answerImportTemplate before any export header is set; both existing gates (enforceApiAccess export, enforceExportPermission) run first. BLOCKED on one PM-only item: the card's FLS row is WRITE permission, and ISecurityService has no write-side field projection (only getReadableFields); adding one means packages/spec/src/contracts/security-service.ts plus packages/plugins/plugin-security/src/security-plugin.ts, outside the claim's file surface, so I stopped there. The draft narrows by the READ projection instead (no FLS-hidden name ever appears), which leaves a readable-but-not-editable field as a column whose rows the import refuses 403. Premises, measured: the 7 injected columns and their flags match the card's table (getMetaItem on a registry object with systemFields on, real ObjectQL + protocol + better-sqlite3); stripReadonlyFields drops readonly columns on insert with a WARN and the field's default (import answers ok 1); exceljs ^4.4.0 is already a rest dependency; the streaming writer's dataValidation support stays unverified (not used: an ordinary Workbook is written once). The card's claim that owner_id 'is overwritten or stripped' did NOT hold: an import supplying owner_id stores it; plugin-security (step 3.5, code-read) refuses a non-self owner 403 unless the caller holds the transfer grant. Measured per exclusion: readonly and autonumber are stripped; formula makes the commit fail the row; summary is stored then overwritten by the next child write; an AUTHOR-declared hidden:true or system:true field that is not readonly is stored by the import like any other (see open question 2). The number rule the domain:cli pointer describes is at base (released in rest 17.5.0, fb194c7) and the template's number text is derived from it. Deviations from the dispatch: PR body opens 'Part of #18386', not 'Closes', because merging this would close the card with its write-FLS half open (os-dev.md: Part of when the merge should not close the card); zero label and assignee writes, as the dispatch ordered.",
    "tests": "All at HEAD 21aea1a, tree clean. (1) pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 229 passed (229), Tests 4451 passed, 50 skipped (4501). (2) --project repo: 1 file, 8 passed. (3) pnpm --filter @objectstack/rest typecheck: exit 0; check:test-typecheck 0 files / 0 errors; tsc --listFilesOnly on tsconfig.test.json lists import-template.ts and both new test files. (4) New tests: import-template.test.ts 41 tests, import-template-route.test.ts 28 tests (real ObjectQL + ObjectStackProtocolImplementation + better-sqlite3 :memory:, registry system fields on); read-back with exceljs of columns, required marks, list validations at rows 2 and 50001 (none at 50002) resolving to the Instructions ranges, zero data rows (findData never called), zh-CN via ?locale= and Accept-Language; the example row imported back through the real POST /data/:object/import with zero coercion codes (acceptance 5, example row only). (5) Property 3: eight pre-change outputs (csv default, json, xlsx styled, xlsx unstyled, csv fields=, csv header=false, empty csv without projection, empty xlsx with explicit fields) captured at BASE 6bff748 with Date frozen and business timezone UTC, deterministic across two runs and under TZ=Asia/Shanghai and TZ=America/New_York; at HEAD every body (sha256 + byte count, text bodies verbatim) and every header matches, and template=false equals the plain export byte for byte. (6) Ablation, one leg per card row through node scripts/ablation-replace.mjs (anchor hit x1 -> x0, blob changed, restore blob == HEAD 953ee24d6d35 / 1a4bdd53c95d, git diff HEAD empty after each leg; tests import src, no dist involved): system, hidden, readonly, computed, autonumber each turned exactly its row test red (plus the rule-roster test); FLS red in the unit test and in the route both-sides test; ?fields= red; order (push to unshift) red; zero rows (template branch deleted in rest-server.ts) turned 11 route tests red; required-with-default red. (7) Property 5 pins moved: rest-server-closed-query-params.test.ts only; describe title ten to eleven, and the accept-every-name loop sends template=true without its csv baseline; its it-count is unchanged, the loop now covers 11 names. Count delta attributed: +2 files and +69 tests, all from import-template.test.ts (41) and import-template-route.test.ts (28); no other file's count moved. (8) node scripts/pm/dispatch-gates.mjs --ran with exit codes: 62 derived, 62 run, 0 NOT-MEASURED (derived zero; check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET, exit 3, and were re-run exit 0 after turbo build of ./packages/
    ). (9) eslint --no-inline-config --format json over the 5 touched TypeScript files: 5 files, 0 errors, 0 warnings; population from eslint.config.mjs files glob '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' minus NEVER_LINTED; the config enables no type-aware linting (eslint.config.mjs:328) and its only file inputs are two baseline JSONs this diff does not touch, so no untouched file's verdict can move. NOT MEASURED: acceptance 6 (Excel, WPS, Numbers, Google Sheets dropdowns) cannot be measured here; CI verdicts in_progress, not awaited.",
    "mcp_calls": "0",
    "api_writes": "2 through the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches): pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft) and this report comment (POST /repos//issues/18386/comments). Plus git push of the branch (not REST).",
    "open_questions": [
    {
    "question": "Write-permission FLS (the card's 'FLS ∩ 可写' row, dispatch property 4): ISecurityService carries only a READ field projection, so the template cannot ask which fields the caller may edit. How should the write projection arrive? The dispatch carried no decision framework for escalations; options and a recommendation are given plainly and the framework is requested.",
    "options": [
    "A: add an optional getWritableFields(object, context) to ISecurityService, implemented in plugin-security beside computeReadableFields with the same evaluator, requiredPermissions fold and delegator intersection, answering the complement of the step-2.5 non-editable mask; the template intersects it. Cost: claim surface extended to packages/spec/src/contracts/security-service.ts and packages/plugins/plugin-security/src/security-plugin.ts with tests, spec api-surface regeneration, spec and plugin-security minor changesets; a patch round on this PR or a sibling card.",
    "B: land the template with the READ projection as the draft does and file the write half as its own card. Cost: until then a readable-but-not-editable field is a column and every row that fills it is refused 403 at import.",
    "C: fold the write mask inside REST from resolvePermissionSetsForContext. Cost: another copy of the field-mask rule without the requiredPermissions fold and the delegator intersection, the drift the security contract warns against."
    ],
    "recommendation": "A, because it answers from the same source the write gate enforces with, as getReadableFields does for reads; B only if the seat wants the template landed before the contract change."
    },
    {
    "question": "The card's 'system' and 'hidden' rows reach further than any engine behaviour: measured, an author-declared hidden:true or system:true field that is not readonly is stored by the import. Among the injected columns all hidden ones are readonly, and the only non-readonly system one is owner_id, which the import stores and plugin-security refuses 403 for a non-self owner without the transfer grant. Keep both rows?",
    "options": [
    "A: keep both rows as the card states (the draft does this).",
    "B: drop both rows and exclude owner_id by name, so every exclusion maps to a measured drop or refusal.",
    "C: keep 'system' (the spec describes the flag as auto-injected, and owner_id's refusal is real for the default caller) and drop 'hidden' (an author-hidden writable field, such as an integration key, is a real import target)."
    ],
    "recommendation": "C, because it keeps the card's criterion (each exclusion is a real drop or refusal) without losing an importable field; A is what ships until decided."
    },
    {
    "question": "Required marks: the draft puts ' ' only on a required field with NO defaultValue, because the engine fills the default before the required check and a blank cell there imports fine. The card says required columns carry ''. Keep the narrower mark?",
    "options": [
    "A: keep it (the Instructions note 'a row that leaves one blank is refused' then stays true).",
    "B: mark every required field."
    ],
    "recommendation": "A, because the mark then states exactly what the import refuses."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/data/:object/import with a column for a formula field: dryRun answers ok 1 / errors 0, and the commit fails the row with code SQLITE_ERROR and the driver text 'table proj has no column named doubled'; the create door POST /api/v1/data/:object with the same key answers 400 INVALID_FIELD (measured: real ObjectQL + ObjectStackProtocolImplementation + better-sqlite3 :memory: at 6bff748, non-system context) · evidence: the import writes through createManyData/createData and the formula key reaches the driver, while the create door refuses it as an unknown field · Seam: runtime:import-runner create path vs the create door's INVALID_FIELD refusal · dedupe words: import formula column SQLITE_ERROR; import dry run commit disagree computed field; import INVALID_FIELD formula",
    "carrier: objectstack-ai/objectui#9600 · the SDK's data.export(object, opts) in packages/client/src/index.ts has no template option; a client calls the URL directly until one is added · noted, not filed",
    "carrier: 承接者:无 · the import reads a trailing % by removing it (25% reads 25), so on a fraction-stored percent field (percentScaleOf fraction) the value stored is 100 times what the cell meant; read-only inference from import-coerce.ts and record-validator.ts, not measured · noted, not filed",
    "carrier: 承接者:无 · the card's 'every object carries the 7 injected columns' holds for objects the registry injects into; systemFields:false, managedBy and sys_ objects carry fewer (resolveInjectedSystemColumns table in packages/spec/src/data/injected-system-columns.ts) · noted, not filed"
    ]
    }

  7. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    Ruling 1 on the dev report · 2026-09-29T18:20Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5893328151. Draft PR #20683 at 21aea1a29d.

    Q1, write-permission FLS: escalated to the maintainer. The seat confirmed the dev's reading on main: ISecurityService (packages/spec/src/contracts/security-service.ts) declares getReadableFields and has no write-side projection. The card's column rule asks for FLS ∩ writable. Meeting that needs a new contract method with its plugin-security implementation (option A), or a split that lands the template on the read projection first (option B). That choice changes the shape of the security contract and this card's scope, so the seat put it to the maintainer in its session. The PR stays a draft until the answer comes.

    Q2, the system and hidden exclusions: A, keep both, as the card states. The card's acceptance item 1 names both exclusions. The dev measured that an author-declared hidden: true field that is not readonly is stored by the import. That finding goes to the maintainer together with Q1, because it is a question about the card's own rule, not about this implementation.

    Q3, the required mark: A, keep the narrower mark. * goes on a required field that has no default. A required field with a default imports fine when left blank, because the engine fills the default before the required check. The card's own criterion is that a column says what the import actually does, and a mark on such a field would say otherwise.

    The PR opens with Part of #18386: kept for now, because merging must not close the card while the write half is open. The seat revisits this line once Q1 is answered.

    Out-of-scope findings: the import/create disagreement on a formula column (the dry run accepts it, while the commit fails with the driver's error) goes to its own card after the seat checks it. The other three notes are recorded here as the dev wrote them.


    Generated by Claude Code

  8. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-decision-facets

    决策请求:导入模板的「能填的列」按哪份权限算?作者标为隐藏、但实际能写入的字段,要不要进模板? · 2026-09-30T03:29Z

    domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),本卡认领者(认领 5893328151)。草稿 PR #20683(head 21aea1a29d)已按卡片做完,只剩下面两个问题。
    ⚠️ 这两个问题从 2026-09-29T18:20Z 起只在聊天里问过,没有落卡。这违反了「卡先于弹窗」,本评论补上;本卡同时转入 needs-user-decision。

    一句话问题

    • Q1: 模板应只列「这个人导入时真能写进去的列」,但平台今天只能回答「这个人能读哪些列」,回答不了「能写哪些列」。
    • Q2: 卡片规定模板排除所有「隐藏」列,但实测:作者标为隐藏、又没设只读的字段,导入时会正常写入。

    Governing text

    • 本卡正文的判据:「这列我填了,导入后真的会落库吗?答"会"才进模板。」列规则表的 FLS 一行写「∩ 可写」;验收 1 点名排除 hidden。
    • packages/spec/src/contracts/security-service.ts:10-15:这个契约面的目的就是「不漂移」。调用方自己推导权限,会在执行路径一变时漂移,所以应该问这个服务。
    • 同一文件 :298 只声明了 getReadableFields,没有写侧的字段投影。
    • packages/plugins/plugin-security/src/security-plugin.ts:5684-5687:写入时,有对象的增删改权限、但对某字段没有 editable 的调用方,只要请求里写了那个字段,就被拒为 PERMISSION_DENIED。
    • 协议声明:Q1 的 A 给安全契约加一个方法,扩大公开面(Clause-②: yes)。B、C 不改契约。Q2 只改本卡的列规则,不改协议。

    前提(每条带复查方法)

    1. 契约里没有写侧字段投影。复查:git show origin/main:packages/spec/src/contracts/security-service.ts | grep -c getWritableFields 应为 0(本席 2026-09-30T03:29Z 读数为 0)。
    2. 草稿按「可读」收窄。复查:分支 claude/issue-18386-export-import-template 的 packages/rest/src/rest-server.ts:10103 调的是 getReadableFields。
      • 后果(dev 实测,报告 5894515898):能读但不能改的字段会出现在模板里,填了它的行导入时被拒 403。
    3. 作者声明 hidden: true 而非只读的字段,导入会正常写入(dev 实测,同一报告)。
      • 平台注入的系统列里,隐藏的都是只读。
      • 唯一非只读的系统列是 owner_id:导入会写入,但写的不是本人、又没有转移授权时,被 plugin-security 拒 403。

    Q1 选项与代价

    选项 做什么 客户看到的后果
    A 契约加 getWritableFields,plugin-security 用读侧同一套求值器实现,模板用它收窄 模板只列真能写的列。本 PR 多一轮:契约、插件、测试和两个 minor changeset
    B 先按「可读」落地模板,写的一半另开卡 马上能用。但「能看不能改」的字段会出现在模板里,填了就整行被拒 403:响亮,不静默
    C 在 REST 里自己算写权限 马上完整。但它复制了一份权限规则,缺委托人交集等步骤,日后会和真实写闸不一致

    业务直译:A = 下载的模板永远和系统的真实写权限一致;B = 先给一个能用的模板,配了字段级「只读权限」的组织,导入会在那几列上报错;C = 模板自己猜权限。

    Q2 选项与代价

    选项 做什么 客户看到的后果
    A 保留卡片原规则,排除所有 hidden 和 system 列(草稿现状) 作者隐藏、但要导入的字段(如对接外部系统的编号)不在模板里,用户得自己加列
    B 两条都去掉,只点名排除 owner_id 每条排除都对应一个实测的丢弃或拒绝,规则变成点名
    C 保留 system,去掉 hidden 符合卡片自己的判据:隐藏但可写的字段进模板

    业务直译:A = 模板更短,少数隐藏字段要手加;C = 模板里有什么就能导什么,按卡片判据一条不多一条不少。

    四轴(业务立场)

    • 长远合理性: Q1 选 A,模板与写闸同源,这正是安全契约「不漂移」的设计。Q2 选 C,每条排除规则都对应一个真实行为。两年后的样子:模板由平台权限服务决定,不由导出路由自己判断。主流参照(本席未实测):Salesforce 的 Data Import Wizard 按字段级权限只允许导入当前用户可编辑的字段。
    • 实际业务拉动: 模板的用户是做批量导入的业务管理员。卡片实测的问题(系统列、只读列被放进模板)今天就在发生。配了「可读不可写」字段级权限的组织有多少,本席没测。
    • 防 AI 犯错: Q1 的 B 出错时整行 403,响亮。C 出错时模板与真实权限不一致,没人会发现,是静默的,不荐。
    • 创业阶段不扩散: Q1 的 A 加一个契约方法,是永久义务;但它是读侧已有方法的对称补全,不是新概念。

    推荐

    • Q1:A。 只看①选 A;②③④ 是否翻转:否。回退:B(如果要模板先上线)。置信缺口:有多少组织配了「可读不可写」没测;插件实现量只按 dev 的估计。
    • Q2:C。 只看①选 C;②③④ 是否翻转:否。回退:A(卡片原文)。置信缺口:真实部署里作者隐藏的字段有多少是导入目标,没测。

    裁后执行(维护者只裁方向)

    • Q1=A: 本席把认领的文件面扩到 security-service.ts、security-plugin.ts、测试、api-surface 重生成和两个 minor changeset。同一个 dev 在 PR feat(rest): GET /data/:object/export?template=true answers an xlsx import template (#18386) #20683 上做第二轮,然后达档复核、落地。PR 首行改为 Closes #18386。
    • Q1=B: 本 PR 按读投影落地,保留 Part of #18386;写的一半本席另开卡。
    • Q2: 随同一轮改列规则;卡片验收 1 的写法由本席按裁定改正文。

    四棱

    ① 项目长远合理性:Q1 的 A 缩小特例(权限只有一个来源);Q2 的 C 去掉一条没有行为支撑的排除。
    ② 实际业务拉动:做批量导入的业务管理员今天就会撞上;字段级「可读不可写」的组织数未测。
    ③ 防 AI 犯错:Q1 的 A 与 B 出错都是响亮拒绝;C 是静默漂移,不荐。
    ④ 创业阶段不扩散:A 加一个契约方法(读侧的对称补全);B、C 不加。
    Prior rulings read: getWritableFields, writable fields, write-side field projection → 0 hits in docs/adr; ADR: none found (git grep -n -i -E "writable ?fields|getWritableFields" origin/main -- docs/adr); thread: 本卡 6 条评论,其中本席裁定 5896083518 把两题上交维护者。
    推荐:Q1 A,Q2 C。只看①选 A / C;②③④ 是否翻转:否。置信缺口:见上。


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #251 item 2 · letter Q1 A · Q2 C · maintainer 「同意」 2026-09-30T05:38Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「同意」 to batch #251 as presented (this card as item 2, with #20281 and #18164); 「同意」 is to the director's recommendation in chat, which adopted the seat's decision request 5903489348 (Q1 A, Q2 C) after this seat's own reading: packages/spec/src/contracts/security-service.ts:298 declares getReadableFields and no write-side projection, and the contract's own preamble says its purpose is that callers do not derive permissions and drift; plugin-security's write gate (step 2.5) refuses a payload naming a field the caller cannot edit; the dev's measurements in report 5894515898 (an author-declared hidden: true writable field IS stored by the import; every platform-injected hidden column is readonly; owner_id is system).

    Ruled: Q1 A. ISecurityService gains getWritableFields(object, context) — the write-side twin of getReadableFields, with the same advisory, fail-soft semantics the contract already states for projections — plugin-security implements it with the read-side evaluator, and the template narrows its columns by it: the template lists exactly the columns the caller can write. ⛔ Not C (a second copy of the permission rule in REST drifts silently). B (ship on the read projection, write half on a new card) is the recorded fallback, not ruled.

    Ruled: Q2 C. The column rule keeps the system exclusion and drops the hidden exclusion: an author-declared hidden: true field that is writable enters the template, because the card's own criterion is "will the value land?" and the import stores it. owner_id stays out through system. Every remaining exclusion — system, readonly, formula / summary, autonumber, FLS ∩ writable — corresponds to a measured drop or refusal.

    Readings recorded with the ruling:

    • Long-run: one source of permission truth; the template is decided by the platform's permission service, never by the export route.
    • Pull: the administrators who bulk-import hit the injected-column defect today.
    • AI-safety: A refuses loudly (403 names the field); C would drift silently.
    • Startup scope: one permanent contract method, the symmetric completion of the read side; no new concept.

    Execution parameters (ruled in the same stroke):

    • The same dev runs round 2 on PR feat(rest): GET /data/:object/export?template=true answers an xlsx import template (#18386) #20683. The claim's file surface widens to packages/spec/src/contracts/security-service.ts, packages/plugins/plugin-security/src/security-plugin.ts, their tests, the api-surface regeneration and two minor changesets (@objectstack/spec — Clause-②: yes, the contract widens — and @objectstack/plugin-security). The PR's first line becomes Closes #18386.
    • A security service that does not implement the new method is detected the way the contract already prescribes for advisory projections (typeof svc.getWritableFields === 'function'), and the template then narrows by the read projection, stated in the response as the contract's soft-fail rule requires — never a silent widening.
    • Q2: the column rule drops hidden; the seat corrects the card's acceptance item 1 on the body under the Ruled line; the pins move with it.
    • Acceptance 6 (Excel / WPS / Numbers / Google Sheets dropdowns) stays a hand check before the card closes.
    • Confidence gaps recorded: how many organizations configure read-not-edit FLS, unmeasured; the plugin implementation size is the dev's estimate; how many author-hidden fields are import targets, unmeasured.

    State: needs-user-decision → pm:dispatched (the claim 5893328151 by domain:spec seat 5 stands; assignee os-justin); the Ruled line goes on the body in the same stroke. objectui#9600 (the console's download-template button) stays blocked on this card's landing and release.

  10. 22 remaining items

  11. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 26 (the fix for the non-conformity the acceptance-6 machine verification 5914688255 found) · 2026-09-30T15:46Z
    Session: session_01Sfe5YjBLwB9J3y8fvm2xq1
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-18386-template-option-default
    Worktree: objectstack-issue-18386-fix
    Domain: domain:spec
    Seat: domain:spec#5 (seat post #19357)
    Clause-②: no (the template's required mark and one xlsx attribute; no accept set moves)
    Scope:

    1. The required mark. packages/rest/src/import-template.ts isTemplateRequired must mirror the engine's own insert-time default (packages/objectql/src/engine.ts resolveOptionDefault). A required field whose options carry a default: true gets no *, and its instructions row does not say it is required. Everything the instructions sheet says about starred columns ("a blank row is refused") must hold for every starred column.
    2. The errorTitle on each list validation is truncated to Excel's 32-character limit. The error text is already capped at 255.
    3. Pins: a fixture with a required select whose option carries default: true. It is unstarred, and a blank cell imports to the default through the real import door. Another fixture has a label longer than 32 characters; its errorTitle is 32 characters or fewer.
      File surface (stop on breach and explain it in the report): packages/rest/src/import-template.ts, its unit and route tests, and one .changeset/18386-*.md (@objectstack/rest patch).
      Container & model: S, mode:subagent, model: opus.
      Review: an at-tier contract review is owed before enqueue.
      Thread-read: 5914688255
      Serial constraints cleared: read at this stamp, no open PR touches packages/rest/src/import-template.ts.
      Then: the machine verification re-runs on the landed fix. Per the maintainer's A, the card closes once that passes.

    Generated by Claude Code

  12. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 18386,
    "status": "done",
    "branch": "claude/issue-18386-template-option-default",
    "pr": "#20904",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (mode:subagent, so this is the dispatching seat's id)",
    "premise_still_valid": true,
    "summary": "Engine semantics (packages/objectql/src/engine.ts, read at base 4edb614). applyFieldDefaults runs on INSERT only. It fills a field that is omitted or null: defaultValue wins whenever defaultValue != null, so '' is a real default and null is none. Otherwise it falls back to resolveOptionDefault, which reads only the canonical default === true, applies to any field type that has an options array, skips a marked option whose value is null or missing, and skips string options. A multi-valued field (declaredMultiValued: isMultiValueField over type plus multiple === true) gets every marked value as an array; a single-valued field gets the first marked value. validateRecord runs required after defaults. It never runs it on system or readonly fields or on autonumber, and it refuses null, a blank string, and [] on a multi-valued field. Before: isTemplateRequired = def.required === true && def.defaultValue === undefined. After: not required unless required === true. False for system, readonly and autonumber. Otherwise true exactly when templateInsertDefault(def) (defaultValue when != null, else the option default as above) is empty for required. Why a copy and not a reuse: resolveOptionDefault and applyFieldDefaults are private members of the ObjectQL class, and @objectstack/objectql is only a devDependency of @objectstack/rest. Moving the rule into spec would go outside the file surface. The copy is held to the engine by a parity test through the real import door. The system, readonly and autonumber exemption goes one step past the dispatch's wording. It only matters for columns named in an explicit ?fields= list, and it is needed for property 2: under the old predicate, such a required column was starred although the import accepts a blank in it, and the parity test measures exactly that. The instructions sheet's Required column follows the same predicate (No / 否 for a column filled by a default). The notes sentence 'a row that leaves one of them blank is refused' now holds for every starred column in the 19-shape test. errorTitle: c.header.slice(0, TEMPLATE_VALIDATION_ERROR_TITLE_MAX) (32), written like the existing error cut, which now reads TEMPLATE_VALIDATION_ERROR_MAX (255). Changeset: .changeset/18386-template-required-option-default.md, @objectstack/rest patch, Clause-②: no. Files touched: exactly the 4 on the claim's surface. PR assignee os-justin, set and read back.",
    "tests": "All runs went through os-verify-lock unless noted, and verdicts are read from its VERDICT line. (1) Build: pnpm --workspace-concurrency=2 --filter '@objectstack/rest^...' build gave exit 0. After merging origin/main into the branch (merge daf0aa8; spec moved, no conflicts, no lockfile change, no regen pending): pnpm install --frozen-lockfile exit 0, then --filter '@objectstack/rest...' build exit 0 (held 4m28s). (2) Pins, verbose run at daf0aa8: pnpm exec vitest run --project local --maxWorkers=2 --reporter=verbose src/import-template.test.ts src/import-template-route.test.ts gave 'Test Files 2 passed (2) / Tests 88 passed (88)'. These include: the route test 'is not starred, its instructions row says not required, and a blank cell imports to the default' (template header ['Title *','Status'], instructions row D6 'No', the filled xlsx sent through POST /data/ticket/import with the Status cell blank reports total 1 ok 1 errors 0 created 1, stored status 'backlog'); the route test 'for every shape of default the engine reads' (19 shapes; for each, the header from GET export?template=true&fields=f is starred exactly when a JSON row with f '' through the real import door is refused with code 'required' on field f, and each filled blank stores what templateInsertDefault predicts, e.g. multiselect ['a','c'], select multiple ['b'], boolean false; measured: '' default and multiselect [] default are refused, readonly, system and autonumber are accepted); the unit test 'a dropdown's error title is cut to Excel's 32 characters, and its message to 255' (a 46-character label gives errorTitle '(label *)'.slice(0,32), length 32; a short title is written whole); and the unit tests for isTemplateRequired and templateInsertDefault. (3) Ablation, on committed HEAD cc26158: node scripts/ablation-replace.mjs put back return def?.required === true && def.defaultValue === undefined; (anchor x1 to x0, replacement x0 to x1, blob c5f1888b2aee to 24d5a851b719; on-disk grep during the run: new-line count 0, old-line count 1). Result: 'Tests 7 failed | 81 passed (88)'. Red: 4 isTemplateRequired unit tests, the describeTemplateColumns option-default test, the route option-default test (got ['Title *','Status *']), and the parity test, which listed 11 disagreements (option_default, option_default_first_wins, option_default_multiselect, option_default_select_multiple, option_default_any_type: starred but accepted; default_value_null, default_value_empty_string, default_value_empty_array: unstarred but refused; readonly, system, autonumber: starred but accepted). The observed direction is the ordinary one: green turned red. Restore: the tool restored the file and printed 'blob == HEAD (c5f1888b2aee) and git diff HEAD is empty'. My own trap then checked out HEAD again and printed 'RESTORE PROVEN'. The subject resolves through relative source imports (./import-template.js, ./rest-server), not through a package export, so dist and ablation-dist-preflight do not apply. (4) Package at daf0aa8: vitest run --project local --maxWorkers=2 (the package's pnpm test) gave 'Test Files 238 passed (238) / Tests 4664 passed | 106 skipped (4770)'. pnpm --filter @objectstack/rest typecheck exit 0: 'check:test-typecheck: OK, 0 file(s) / 0 error(s)'. (5) Gates: node scripts/pm/dispatch-gates.mjs --commands derived 60 commands, the same list before and after the merge (4 changed paths, +313/-7). All 60 ran at daf0aa8, and --ran reconciled them: '60 derived, 58 run, 2 NOT-MEASURED, 0 UNRUN'. Among the 58 green: check:nul-bytes 'OK (scanned 9527 text file(s) ... no raw ASCII control bytes)', check:cross-package-test-inputs, check:test-source-alias, check:doc-authoring, check:engine-double-contract, check:published-files, check:type-check-coverage, and the changeset gates (adr-0087-registration, changeset-no-major, empty-changeset). NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt; reason: exit 3 PREREQUISITE NOT MET, both need a whole-repo build (42 packages have no dist); declared to CI. (6) Lint, narrowed and proven: eslint --no-inline-config --format json on the 3 changed .ts files at daf0aa8 gave 3 files, 0 errors, 0 warnings. Which files eslint checks comes from eslint.config.mjs's own files globs: the .ts files match, and the changeset .md is outside them (eslint answered 'File ignored because no matching configuration was supplied'). The count comes from the JSON output. The config has no parserOptions.project or projectService, so no rule reads types across files, and this diff cannot move the lint result of any untouched file. The full pnpm lint over the repo is left to CI. (7) CI after push, one read of check-runs on head daf0aa8, deduplicated by name: 17 names. 6 success (Auto Label, Check Documentation Links, Check PR Size, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card), 1 skipped (Packed-tarball smoke (opt-in)), 10 in_progress (Check Changeset, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter). Of the seven required contexts, Lint & Repo Gates and Governed Surface Queue Guard were in_progress, and TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core and Temporal Conformance had not been created yet. CI status: in_progress; not polled.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes, each sent as one POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204) through scripts/pm/fleet-write and executed by the relay as objectstack-fleet[bot]: (1) pr_create, request fw-20260930T170235Z-ca1cce, run 36748579186, success: POST /repos/objectstack-ai/objectstack/pulls (draft forced) opened #20904; body read back, 3371 bytes sent and 3371 stored, identical. (2) label-write --assign os-justin, request fw-20260930T170315Z-e8ec1a, run 36748658451, success: POST /repos//issues/20904/assignees; read back 'MATCHES the target'. No labels written: the dispatch named none, and the diff publishes a changeset, so skip-changeset does not apply. The labels documentation, size/m, tests and tooling were already on the PR from the platform's labelers. (3) This os-dev-report comment, via scripts/pm/post-stamped.mjs: POST /repos//issues/18386/comments. Not REST: 4 git pushes (empty-branch probe, fix, changeset, merge of origin/main).",
    "open_questions": [
    {
    "question": "The unreleased changeset .changeset/18386-export-import-template.md (the template's minor) says the header carries ' ' 'after each field that is required and has no default value'. This round adds the option default and the system/readonly/autonumber exemption to what 'default value' means. Does that sentence need a change before release? (Editing it would be outside this round's file surface, so I left it.)",
    "options": [
    "A: leave it. Read loosely it is still true, and the new patch changeset, which ships in the same release, states the exact rule.",
    "B: the seat changes it to 'required and filled by no default (defaultValue or an option marked default: true)' in its own small edit."
    ],
    "recommendation": "A, because both changesets compile into the same release notes, the patch one names every case, and a second edit to a card-owned changeset costs another write and review with no change for the reader."
    }
    ],
    "out_of_scope_findings": [
    "carrier: the change that lands server-side resolution of picklist references (the picklist kind's follow-up; spec kind added in commit addbbf0) · noted, not filed. On main nothing at runtime resolves a picklist reference into options, so the object read the template uses and the engine's registry schema both see no options and agree today. When resolution lands it must reach both, or a picklist option marked default: true would unstar a column the engine still refuses; the parity test in import-template-route.test.ts is the place to add a picklist shape. Dedupe words: picklist resolution, option default, import template required, resolveOptionDefault.",
    "carrier: none · noted in the PR's Acceptance notes only. A before
    hook that fills a field, and an expression default that fails to evaluate at insert (the engine leaves the field unset and logs a warning), cannot be seen from metadata. The template counts every default an author declared, if it is not empty, as filling the field.",
    "carrier: none · noted in the PR's Acceptance notes only. The 32 and 255 cuts count UTF-16 code units, so a label whose 32nd unit falls inside a surrogate pair would keep half of the pair. No shipped label reaches that."
    ]
    }

  13. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Ruling on the fix report 5915980985 · 2026-09-30T17:07Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5914714137. Draft PR #20904 at daf0aa882a.

    Accepted as delivered:

    • The required mark now follows the engine's insert-time defaults. A value comes from defaultValue when it is not null, and otherwise from the options marked default: true, the first one for a single-valued field and all of them for a multi-valued field. system, readonly and autonumber columns are never starred, because validateRecord never refuses a blank in them.
    • The copy is pinned to the engine through the real import door. The parity test covers 19 default shapes, and an ablation turns 11 of them red.
    • errorTitle is cut to 32 characters. The instructions sheet's Required column follows the same predicate.
    • The exemption for system / readonly / autonumber goes one step past the claim's wording. It holds because it is what makes "a blank starred cell is refused" true for every starred column.

    Open question (the unreleased template changeset's wording): A. It stays as it is, because the patch changeset in the same release states the exact rule.

    Out of scope: when picklist references resolve at runtime, the template's object read and the engine's schema must both see the resolved options. The parity test is where a picklist shape is added. That belongs to #19519, and a pointer goes there.

    Next: needs:contract-review goes on the PR and this card. The at-tier review runs once CI is green. After the fix lands, the machine verification runs again. It passes if the * on showcase_task.status is gone, and on a pass the card closes per the maintainer's 「A,机器验证通过就直接关卡」.


    Generated by Claude Code

  14. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Landed: PR #20904 → 6f1f1c1035 (the fix for the non-conformity); the machine verification re-runs now · 2026-09-30T17:55Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5914714137.

    • Landed: PR fix(rest): the import template stars a column only when the import refuses a blank in it #20904 merged through the merge queue at 2026-09-30T17:53Z as 6f1f1c1035, with one parent, 165c1d49e3.
    • Content check: each of the 4 files has the same git patch-id --stable as the reviewed head daf0aa882a against its merge base 33b6e8bece.
    • Review: the at-tier PASS 5916304647 was taken at daf0aa882a, which is the head that landed.
    • Delivered:
      • The required mark follows the engine's insert-time defaults, defaultValue or an option marked default: true.
      • system, readonly and autonumber columns are unstarred.
      • errorTitle is cut to 32 characters.
      • A parity test over 19 shapes runs through the real import door.
    • Next: the verification agent re-runs steps 1, 2 and 4 of 5914688255 on main, including 6f1f1c1035. The pass condition: showcase_task.status is unstarred, a blank status imports as backlog, every other reading holds, and every errorTitle is 32 characters or fewer. On a pass, the card closes per the maintainer's 「A,机器验证通过就直接关卡」. Step 3 (LibreOffice) waits for the maintainer's A/B on installing Calc.

    Generated by Claude Code

  15. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    验收 6 机器验证(修复后复跑):第 1、2、4 步全部通过 · 2026-09-30T18:21Z

    domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1)。本轮复跑的被测树是 main 6f1f1c1035,即修复提交 PR #20904 本身,也包含 e5c7d07ed4。测法与 5914688255 相同:用最小服务挂真实的 RestServer 路由,加载 showcase 的对象和翻译包。复现脚本在本席 scratchpad verify-18386/repro.sh,读数在 out/。

    1. 状态列不再带 *。
      • zh-CN 表头为 状态,「必填」一栏为「否」;en 表头为 Status,对应一栏为 No。
      • 仍带 * 的只剩 标题 * 和 项目 *。
      • 检查脚本的判据已改为与引擎一致;拿新判据复查上一轮 2d5fe76f43 的模板,只在状态列报错,说明它确实能抓到旧缺陷。
    2. 状态留空的行被接受。 zh-CN 与 en 各导入一行,都经真实导入接口,结果 ok 1,落库值为 status=backlog。
    3. errorTitle 不超过 32 字符。 showcase 里最长的是 11 字符。另用一个临时夹具验证截断:34 字的中文标签和 57 字的英文标签,都被截到 32 字符,表头仍保留完整标签。
    4. 其余读数不变。 DV 结构、sqref、formula1、区域值和 error 文本逐项相同,唯一的差别是状态列的表头与 errorTitle。4 份模板加 6 份改后文件都通过 XSD 校验;往返导入全部 ok,没有 invalid_option / invalid_boolean。
    5. 每个带 * 的列,留空都会被拒绝。 每次只清空一个带星的格子,再经真实导入接口导入:title、project、name、f_master_detail 都以 required 被拒;不清空的对照行都是 ok。

    结论:第 1、2、4 步通过。 第 3 步(用 LibreOffice 打开)还等维护者决定是否安装 Calc。若不安装,本卡按维护者「A,机器验证通过就直接关卡」直接关闭。

    语言绑定(zh-CN 模板必须用 zh-CN 请求导回)的读数与上一轮相同,已转告 objectui#9600(5915156263)。模板挂在哪道权限闸后面,另见决策卡 #20896。


    Generated by Claude Code

  16. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    关卡:维护者选 B,不补测 LibreOffice,按「A,机器验证通过就直接关卡」关闭 · 2026-09-30T22:42Z

    domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),认领 5914714137 的持有者。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions