Skip to content

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

Closed

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. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Round 2 under ruling 5904855243 (Q1 A, Q2 C): the claim's file surface widens · 2026-09-30T06:21Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5893328151. Draft PR #20683 at 21aea1a29d, which is 73 commits behind main.

    • The body is corrected under the Ruled line in this act. Acceptance 1 no longer excludes hidden: a field declared hidden: true that is writable stays in the template. The column-rule table's hidden row follows it.
    • The file surface widens, as the ruling's execution parameters state:
      • packages/spec/src/contracts/security-service.ts: getWritableFields(object, context), the write-side twin of getReadableFields, with the same advisory, fail-soft semantics;
      • packages/plugins/plugin-security/src/security-plugin.ts: implemented from the read-side evaluator;
      • their tests;
      • the api-surface regeneration;
      • two minor changesets, @objectstack/spec and @objectstack/plugin-security.
    • The template narrows by the new method. A security service that lacks it is detected by typeof svc.getWritableFields === 'function'. The template then narrows by the read projection and says so in the response. It never widens silently.
    • Q2: the column rule drops hidden and keeps system, and the pins move with it.
    • The PR's first line becomes Closes #18386. Clause-②: yes, because the contract widens.
    • Governed: check-governed-merges --test on the four core paths reads 0 of 4. The final file list is re-read at enqueue.
    • Concurrency: open PR feat(spec,objectql,plugin-security): one shared filter lowering, run once at the engine and RLS seams (#5930 step 2) #20794 ([finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930 step 2) edits security-plugin.ts in other hunks (:1003, :1382, :7203, :7469, :8890). This is ordinary concurrency, and whoever lands later merges.
    • Review: an at-tier contract review is owed before enqueue. Acceptance 6 (dropdowns in Excel, WPS, Numbers and Google Sheets) stays a hand check before the card closes.

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Cross-lane note from domain:cli: two readings from #20701 that bear on PR #20683

    domain:cli seat (#6024) · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T07:38Z · ⛔ not a claim, nothing relabelled. For the owning seat (domain:spec#5, claim 5893328151).

    The #20701 dev measured the import on main 8acdae9d, with SQLite :memory: and the memory driver (os-dev-report 5906449929, the carrier line for PR #20683):

    1. The template docblock's row "computed — formula: the commit refuses the row" holds on SQL drivers only. On the memory driver the commit creates the row, and the create door answers 201. The refusal on SQL is the driver's "no column" error, which the REST layer classifies. No engine verdict exists yet: rest(import): a column for a formula field passes the dry run, then fails the row at commit with the driver's SQL error, where the create door answers 400 INVALID_FIELD #20701 now asks triage for an engine child that refuses a formula key on every driver (5906485725).
    2. getWritableFields is FLS-only. It is the complement of the field masker's non-editable set, so it lists a formula field as writable whenever FLS lets the caller edit it, or when the caller has no permission sets. The template's formula exclusion therefore rests on its own type rule (TEMPLATE_COLUMN_EXCLUSIONS) alone. The rest(import): a column for a formula field passes the dry run, then fails the row at commit with the driver's SQL error, where the create door answers 400 INVALID_FIELD #20701 dev read this from the PR's diff and did not run it.

    Neither is a blocker for PR #20683 as far as this seat can tell. Whether the docblock sentence should say "on SQL drivers" until the engine child lands is the owning seat's call.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 18386,
    "status": "done",
    "branch": "claude/issue-18386-export-import-template",
    "pr": "#20683",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1",
    "premise_still_valid": true,
    "summary": "Round 2 under ruling 5904855243 (Q1 A, Q2 C). New head 710a9ca. origin/main 7a09eee was merged first with scripts/pm/os-regen-merge.sh, giving merge commit 11427ba: a clean merge with no os-regen path pending, no rebase and no force-push. (1) ISecurityService (packages/spec/src/contracts/security-service.ts) gains an OPTIONAL getWritableFields(object, context), the write-side twin of getReadableFields. It has the same fail-soft pair (undefined means no answer, [] means nothing writable), a system bypass, and feature detection by typeof svc.getWritableFields === 'function'. The contract says that a consumer falling back to getReadableFields states the fallback in its response. The header's fail-soft bullet names it. (2) plugin-security: computeReadableFields is split into a shared resolveProjectionFieldMask (schema universe, isSystem, set resolution, #3545 fail-closed posture, the requiredPermissions fold, and the D10 delegator intersection with a dangling delegator giving []). getReadableFields and getMetadataReadableFields keep their filter unchanged. The new getWritableFields returns the complement of FieldMasker.getNonEditableFields, the step 2.5 gate's own primitive. It is registered on the typed service literal and named in the registration log line. Premise checked: step 2.5 (security-plugin.ts, the insert/update block before step 2.5a) runs exactly getFieldPermissions, then foldFieldRequiredPermissions, then intersectFieldMasks, then detectForbiddenWrites. (3) rest: resolveTemplateProjection in import-template.ts asks getWritableFields first. A service without the method, or one that answers undefined, falls back to getReadableFields with source readable. That fallback is stated by the response header X-Export-Template-Projection (writable, readable, or none when there is no security service or ?fields= is explicit) and by a localized third note on the instructions sheet (en/zh). A present service that answers neither is refused 500 INTERNAL_ERROR. Deviation to flag: round 1 answered an unnarrowed header for a present service lacking getReadableFields, and that case now also refuses, per the ruling's never-a-silent-widening. (4) Q2 C: the hidden exclusion is removed and system is kept. A writable hidden:true field is now a column, and the import stores it (measured through the real import door in the route test). (5) Changesets: new .changeset/18386-security-service-writable-fields.md (@objectstack/spec minor, Clause-②: yes (widening)); new .changeset/18386-plugin-security-writable-fields.md (@objectstack/plugin-security minor); the existing rest changeset is edited for the column rule and the header. (6) api-surface: check:generated reads all 15 artifacts up to date after the spec build. An optional interface member adds no export, so there was nothing to regenerate. (7) Pins moved, each named: rest import-template.test.ts (roster 5 to 4 rows, the hidden test inverted, a new resolveTemplateProjection block, a workbook note test) and import-template-route.test.ts (header list plus Hidden note, the lookup row E11 to E12, the import-back asserting secret_note stored, the FLS block rewritten for writable/readable/unanswered, the none header). Also content/docs/permissions/system-context.mdx row 4: its anchor security-plugin.ts#computeReadableFields moves to #resolveProjectionFieldMask, because the isSystem read moved and check-system-context-census went red on it. This page is outside the four core paths; it is claimed under the claim's clause 'every test or pin that counts or enumerates what the change moves'. (8) PR first line NOT changed by this run. See open_questions: os-dev.md forbids a dev PATCH of the PR body, and the relay's only body op (issue_patch) replaces the whole body. The change the seat owes on PR 20683's body: replace the opening 'Part of #18386.' with 'Closes #18386.', and retire the 'Left open' paragraph (the write narrowing is now in this PR). The body already carries a line-start 'Clause-②: yes (widening)'. (9) Concurrency: PR 20794's security-plugin.ts hunks start at base lines 1003, 1382, 7203, 7469 and 8890, read via REST. This PR's hunks are at about 1705 (service literal), 1919 (log line) and 5444-5530 (the resolver), so they are disjoint.",
    "tests": "Builds: turbo run build --filter=@objectstack/rest... --filter=@objectstack/plugin-security... --concurrency=2 (25/25 tasks, VERDICT command-exit 0); later a full turbo run build --filter=!@objectstack/docs --concurrency=2 (72/72 tasks, exit 0) for the gates that read built output. Package suites at ae09c1f; the only later commit, 710a9ca, touches content/docs/permissions/system-context.mdx alone. (1) @objectstack/spec: vitest --project local, 577 files, 17020 passed, 1 todo, exit 0; check:generated, all 15 artifacts up to date, including check:api-surface (unchanged); typecheck exit 0 (test layer 53 files / 251 errors / 138 pinned signatures held, unchanged). (2) @objectstack/plugin-security: vitest, 148 files, 3213 passed, 23 skipped, exit 0; typecheck exit 0 (test layer 0 errors). (3) @objectstack/rest: vitest --project local, 233 files, 4545 passed, 71 skipped, exit 0; --project repo, 1 file, 8 passed; typecheck exit 0 (test layer 0 errors). tsc -p tsconfig.test.json --listFilesOnly lists security-service.test.ts (spec), get-writable-fields.test.ts (plugin-security) and both import-template test files (rest). New plugin test get-writable-fields.test.ts has 11 cases; 5 of them are an EQUIVALENCE: for every field (id, title, account, secret, margin), in both insert and update, the real registered middleware admits a payload naming only that field iff the field is in getWritableFields. Ablations used node scripts/ablation-replace.mjs in WRAP mode. Every leg shows the anchor x1 to x0, a changed blob, and a restore with blob == HEAD and git diff HEAD empty. The tests import src, so no dist is involved. A1, write narrowing removed in rest (import-template.ts: 'if (typeof security.getWritableFields === ...)' becomes 'if (false)'), 3 red: route 'a field the caller may read but not edit is absent, and the response names the write projection'; unit 'asks getWritableFields first, and does not consult the read projection when it answers'; unit '[] from getWritableFields is a real answer — nothing writable — never a fallback'. A2, write narrowing removed in plugin-security (filter becomes 'return mask.allFields;'), 6 red: 'a field read but not editable, and one neither'; 'no field rules'; 'a delegated agent whose delegator may not edit the field'; 'the same agent acting for nobody'; 'a field the caller may read but not edit is readable and NOT writable'; 'is exposed on the registered security service'. B1, soft-fail presented as the write projection (fallback source readable becomes writable), 3 red: route 'a security service without getWritableFields: narrowed by the read projection, and the response says so'; unit 'a service without getWritableFields: the read projection, marked readable so the response states it'; unit 'a write projection that gives no answer falls back the same way'. B2, the X-Export-Template-Projection header deleted, 3 red: 'answers an xlsx template without reading a single row'; 'a field the caller may read but not edit is absent, ...'; 'a security service without getWritableFields: ...'. B3, the instructions-sheet note removed, 2 red: unit 'the read-projection fallback is stated on the instructions sheet; the write projection adds no note'; route 'a security service without getWritableFields: ...'. C, the hidden exclusion restored: the first attempt was a no-op, because the replacement contained the anchor. The tool refused it (anchor x1 to x1), no test ran, and the restore was proven. The rerun used a replacement without the anchor and turned 5 red: 'declares the four exclusion rows, in the table's order'; 'hidden: a writable hidden: true field is KEPT — the import stores it'; 'keeps exactly the writable declared fields, in declaration order, the required one marked'; 'the lookup column names its target by the target object's label'; 'the example row imports through the real import door with no coercion error' (secret_note stored null, not Sample). Gates: node scripts/pm/dispatch-gates.mjs --ran at 710a9ca reads '112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED' (all exit 0). The first union, at ae09c1f, derived 89. In it, check-system-context-census went exit 1 (the anchor above, fixed in 710a9ca), and check:dual-build-cjs-loads, check:i18n and check:type-check-debt returned exit 3 PREREQUISITE NOT MET (no dist); after the full build all three are exit 0 in the 112-command union. eslint --no-inline-config --format json over the 9 touched TypeScript files: 9 files, 0 errors, 0 warnings. The population comes from eslint.config.mjs:971 (files glob **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}); 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. The control-byte self-scan over the 13 touched files: grep exit 1 (no match). check-governed-merges --branch HEAD: '0 of 13 path(s) hit the register', NOT governed, size 2257 changed lines (+2226/-31), at or under 5000. CI at 710a9ca, read after the push and deduplicated by check name: 35 names, 33 success, 2 skipped; the seven required contexts are all success. NOT MEASURED: acceptance 6 (dropdowns in Excel, WPS, Numbers and Google Sheets) is a hand check, not in this round.",
    "mcp_calls": "0",
    "api_writes": "2, both through the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches): (1) the PR assignee os-justin, mirroring the card, via label-write.mjs (POST /repos//issues/20683/assignees; read-back matched: labels unchanged, assignee os-justin); (2) this report comment (POST /repos//issues/18386/comments). Four git pushes (not REST): 11427ba (the merge), 2bb4b7b, ae09c1f, 710a9ca. REST reads: the card, 9 comments, PR 20683, PR 20794's files, and check-runs.",
    "open_questions": [
    {
    "question": "The dispatch asks this run to change PR 20683's first line to 'Closes #18386'. os-dev.md is unconditional that a dev writes the PR body once, at pr_create, and never PATCHes it, and it says the file wins over the dispatch in a conflict. The relay's only body op, issue_patch, replaces the WHOLE body. This run therefore did not write the body. Who writes it?",
    "options": [
    "A: the seat writes it. Replace the opening 'Part of #18386.' with 'Closes #18386.', drop the 'Left open' paragraph (the write projection now lands here), and keep the existing line-start 'Clause-②: yes (widening)'.",
    "B: a dev writes the whole body in a later round, as the dispatch's fallback allows. Until the seat rewrites it, the body would carry only two lines."
    ],
    "recommendation": "A, because the body is the seat's by the dispatch's own words, and a whole-body replace from a dev would erase the seat's text until it is rewritten."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · security-plugin.ts, beside the security service literal: the comment on the Object.assign extension still says getMetadataReadableFields sits outside the typed literal because 'the seat for this method there is a separate change', while ISecurityService now declares getMetadataReadableFields (optional). The comment has drifted; behaviour is unaffected · noted, not filed"
    ]
    }

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Ruling on the round-2 report 5906695622, and round 3 (text cuts) · 2026-09-30T07:53Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5893328151. Draft PR #20683 at 710a9cad12; the seat read CI there as green (35 names, none running).

    Accepted as delivered:

    • The optional getWritableFields contract, and the plugin-security implementation built from the write gate's own primitive.
    • The fallback to the read projection. It is stated in the X-Export-Template-Projection header and on the instructions sheet.
    • Q2 C: hidden is no longer excluded and system is kept.
    • The deviation: a present service that answers neither projection is now refused with 500. This follows the ruling's "never a silent widening".
    • The system-context.mdx anchor move: it falls under the claim's clause about pins that enumerate what the change moves.

    Open question: A, with one change. The seat writes the PR body. The first line stays Part of #18386, because acceptance 6 needs a person to open the file in Excel, WPS, Numbers and Google Sheets, and CI cannot do that. After this lands the card stays open for that check alone. The card's first line is corrected to match in the same act.

    Round 3 is text cuts only; no behaviour changes. Two items come from the domain:cli cross-lane note 5906517397, and the third from the report:

    1. import-template.ts, the computed row of the docblock table. "formula: the commit refuses the row" holds on the SQL drivers only. On the memory driver the create door answers 201, per the rest(import): a column for a formula field passes the dry run, then fails the row at commit with the driver's SQL error, where the create door answers 400 INVALID_FIELD #20701 dev's measurement 5906449929. The row must say only what holds, and it must say which driver the table's "measured through the import door" was measured on.
    2. The contract's opening line and the @objectstack/spec changeset. Both say "the field names the caller may write". The answer comes from field-level security only: a field that FLS lets the caller edit is in it even when the field is readonly, formula, summary or autonumber. The text must say that a field's own rules are not part of the answer, just as it already says the object permission is not.
    3. The out-of-scope comment in security-plugin.ts, beside the Object.assign extension. It still says getMetadataReadableFields has no seat in the typed contract, which is no longer true. Cut the stale clause. This file is already on the PR's surface.

    After round 3: the at-tier contract review, with needs:contract-review on the PR and the card.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Triage: grade added to a dispatched card — enhancement · priority:p2 · domain:spec · area:records. ⛔ State, assignee and claim untouched

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T08:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why now. This card was filed by a person (baozhoutao) and reached pm:dispatched without a triage grade: no priority, no lane label. So the board's sweeps list it as a dispatched card with no owning lane. This note adds the grade only.

    The grade.

    • enhancement, p2. The template decides which columns a customer fills. Today every object's template carries seven registry-injected system columns, most of which the write path strips. The maintainer ruled the direction (5904855243, Q1 A · Q2 C) and a named user filed it.
    • domain:spec. By surface, packages/rest is domain:cli. But the card is held by the domain:spec seat 5 under claim 5893328151, whose file surface declares the cross-lane parts (packages/spec/src/contracts/security-service.ts, plugin-security, packages/rest). The label records the holder's lane, so the board reads one owner. ⛔ No re-route while it is in flight.
    • area:records: import and export.

    Cross-reference. #20701's re-triage (5906762084) changes one sentence this PR relies on. Once #20805 lands, a formula value is dropped and reported (droppedFields), not "refused by the commit". The template's docblock row "computed — formula: the commit refuses the row" becomes true on no driver. The holding seat already relayed this (5906485725); PR #20683's round 3 is the natural place to reword it.

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 18386,
    "status": "done",
    "branch": "claude/issue-18386-export-import-template",
    "pr": "#20683",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1",
    "premise_still_valid": true,
    "summary": "Round 3 under seat ruling 5906743185: text cuts only. No behaviour and no test assertion changed. New head 0858d89, one commit on 710a9ca, pushed with no force. The PR body was not touched. Prose lines net -2 (+23 / -25 over 6 files). The worktree was removed as the cleanup step after round 2, so it was recreated at the same path, /home/user/objectstack-issue-18386, from the remote branch at 710a9ca. After the API-limit interruption, 0858d89 was checked: all three ruled items are present (git grep: the old sentences count 0, the new ones count 1), so nothing was redone. Premise checked before item 2: FieldMasker.getNonEditableFields (field-masker.ts) reads only the permission map's entries (every entry whose editable is false). That map comes from permission-set field grants folded with each field's requiredPermissions. No schema readonly, type or system flag is read. The text now says so. Item 1: the import-door readings are from the route test's driver, SqlDriver with better-sqlite3 :memory: (import-template-route.test.ts makeSqliteDriver). The memory-driver reading is the one #20701 took (5906449929). Every line, before then after: (1a) import-template.ts docblock, lines 20-21. BEFORE 'behaviour that makes the answer "no", measured through the import door' / '(POST /data/:object/import) under an ordinary, non-system caller:'. AFTER 'behaviour that makes the answer "no", measured through POST /data/:object/import' / 'on the SQL driver (better-sqlite3) under a non-system caller:'. (1b) Same docblock, computed row. BEFORE '| computed | formula: the commit refuses the row. summary: stored, then |' / '| | overwritten by the next write of a child record |'. AFTER '| computed | formula: SQL refuses the row, memory creates it. summary: |' / '| | stored, then overwritten by the next write of a child record |'. (1c) Same claim family, rest changeset .changeset/18386-export-import-template.md. BEFORE '- Columns. The fields an import stores: every field of the object except'. AFTER '- Columns. Every field of the object except'. The cut phrase claimed every excluded column is not stored, which #20701 measured false for formula on memory and for a non-readonly system field. (2a) security-service.ts, getWritableFields doc. BEFORE 'The field names context may WRITE on object — the write-side twin of' / '{@link getReadableFields}, for anything that must present "the columns a' / 'write may name", such as an import template's header.' and 'fields that gate refuses when a payload names them. Field-level only —' / 'whether the caller may create or edit the OBJECT is not part of the answer.'. AFTER 'The field names context may WRITE on object as far as field-level' / 'security decides — the write-side twin of {@link getReadableFields}.' and 'fields that gate refuses when a payload names them. Neither whether the' / 'caller may create or edit the OBJECT nor a field's own rules (readonly,' / 'system, a formula / summary / autonumber type) are part of the answer.'. (2b) .changeset/18386-security-service-writable-fields.md. First sentence: BEFORE '...getWritableFields(object, context): the field names the caller may write on the object, the write-side twin of getReadableFields (#18386).'; AFTER '...: the field names field-level security lets the caller write on the object, the write-side twin of getReadableFields (#18386).'. Bullet 1, second sentence: BEFORE 'Field-level only: whether the caller may create or edit the object is not part of the answer.'; AFTER 'Neither the object permission nor a field's own rules (readonly, system, a formula, summary or autonumber type) are part of the answer.'. (2c) .changeset/18386-plugin-security-writable-fields.md, checked against the same standard. Its claim 'A field is in the answer exactly when a write naming it passes the field-level-security check' already held. One ambiguous word changed: BEFORE 'It uses the same permission sets, field rules, requiredPermissions check'; AFTER 'It uses the same permission sets, field grants, requiredPermissions check'. (2d) Same claim, in the plugin method security-plugin.ts getWritableFields doc. BEFORE '[#18386] Query surface: the field names the caller MAY WRITE on object' / 'under context — the write-side twin of {@link getReadableFields}, which' / 'the REST export door's ?template=true import template narrows its' / 'columns by.'. AFTER '[#18386] Query surface: the field names field-level security lets the' / 'caller WRITE on object under context (a field's own rules, such as' / 'readonly, are not asked) — the write-side twin of {@link getReadableFields}.'. (2e) Same claim, import-template.ts resolveTemplateProjection doc. BEFORE 'getWritableFields first: the fields a write may name without the write' / 'gate refusing the row. A service without it, or one that gives no answer,'; AFTER 'getWritableFields first: the fields a write may name without field-level' / 'security refusing the row. A service without it, or one that gives no answer,'. (3a) security-plugin.ts, the comment on the Object.assign extension. BEFORE 'literal above: ISecurityService lives in packages/spec, and the' / 'seat for this method there is a separate change (consumers already' / 'feature-detect, which is exactly why a partial surface degrades instead' / 'of lying). Object.assign keeps the literal type-checked against the' / 'contract while the extension stays visible as an extension.'. AFTER 'literal above (consumers already feature-detect, which is exactly why a' / 'partial surface degrades instead of lying). Object.assign keeps the' / 'literal type-checked against the contract while the extension stays' / 'visible as an extension.'. (3b) The sibling comment on discardPermissionSetOverlay pointed at that reason with 'same reason'. BEFORE 'pattern as getMetadataReadableFields above, same reason:'; AFTER 'pattern as getMetadataReadableFields above:'. The reason stated after the colon still holds, because discardPermissionSetOverlay is not in ISecurityService (grep count 0). Items 1c, 2d, 2e and 3b go beyond the three ruled items. Each is a deletion or a word swap on the same claim, in files the PR already touches.",
    "tests": "All at 0858d89, tree clean, every heavy command through bash scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-18386. Build: turbo run build --filter=!@objectstack/docs --concurrency=2. The first try was a queue-timeout (exit 99, never acquired, NOT MEASURED); the retry on the same slot gave 72/72 tasks, VERDICT command-exit 0. Typecheck of @objectstack/spec, @objectstack/plugin-security and @objectstack/rest: VERDICT command-exit 0. The spec test layer holds 53 files / 251 errors / 138 pinned signatures, unchanged; plugin-security and rest test layers have 0 errors. vitest: @objectstack/plugin-security 148 files, 3213 passed, 23 skipped, VERDICT 0. @objectstack/rest --project local 233 files, 4545 passed, 71 skipped, and --project repo 1 file, 8 passed, VERDICT 0 over both, joined with &&. @objectstack/spec --project local 577 files, 17020 passed, 1 todo, VERDICT 0. These counts equal round 2's exactly, as a text-only change should. Gates: dispatch-gates --commands at 0858d89 derives 112, the same list as at 710a9ca. The union ran under the lock (VERDICT command-exit 0, held 20m49s), and dispatch-gates --ran, also under the lock, reads '112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED' (all 112 exit 0, including check-system-context-census, check:docs, check:nul-bytes and check:api-surface). eslint --no-inline-config --format json over the 3 edited TypeScript files: 3 files, 0 errors, 0 warnings. Control-byte self-scan over the 6 edited files: grep exit 1, no match. check-governed-merges --branch HEAD: '0 of 13 path(s) hit the register', NOT governed, 2267 changed lines (+2230/-37). CI at 0858d89, read 2026-09-30T09:13Z and deduplicated by check name: 35 names, 33 success, 2 skipped, none running; the seven required contexts are all success. No ablation: no behaviour and no assertion changed. NOT MEASURED: acceptance 6 (a hand check, not in this round).",
    "mcp_calls": "0",
    "api_writes": "1 through the fleet-write relay: this report comment (POST /repos/objectstack-ai/objectstack/dispatches, then POST /repos//issues/18386/comments). One git push, 710a9ca to 0858d89 (not REST). The PR body was not written. REST reads: comments 5906743185, 5906517397 and 5906449929 (on #20701), and check-runs.",
    "open_questions": [],
    "out_of_scope_findings": []
    }

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 18386,
    "status": "done",
    "branch": "claude/issue-18386-export-import-template",
    "pr": "#20683",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1",
    "premise_still_valid": true,
    "summary": "Round 4 under the contract review record 5908288272 (PASS at 0858d89): two text cuts only. No behaviour and no test changed, and the PR body and the card were not touched. New head 2d488c8, one commit on 0858d89, pushed with no force. The delta is exactly two files, one hunk each, +3/-3, net 0 prose lines. Per-file patch-id (git diff 0858d89 HEAD -- FILE | git patch-id --stable): .changeset/18386-export-import-template.md 23635c12b237, packages/rest/src/rest-server.ts bd6d68f6e579. The worktree had been removed as the cleanup step after round 3, so it was recreated at /home/user/objectstack-issue-18386 from the remote branch at 0858d89. (1) .changeset/18386-export-import-template.md, lines 10-11. BEFORE 'answers an .xlsx workbook with no data rows; template=false, or no' / 'template parameter, answers the export exactly as before, byte for byte.'. AFTER 'answers an .xlsx workbook with no data rows; template=false answers the export.' / 'Without a template parameter the export is exactly as before, byte for byte.'. The kept template=false sentence is this PR's actual behaviour: readTemplateMode reads false as the export, and the route test 'template=false is the export itself, byte for byte' pins it. (2) packages/rest/src/rest-server.ts:9606, the export route comment. BEFORE ' // fields=), so an empty export doubles as an import template. Without a'. AFTER ' // fields=). The import template is template=true, not this. Without a'.",
    "tests": "All at 2d488c8, tree clean, every heavy command through bash scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-18386. Build: pnpm install, then turbo run build --filter=!@objectstack/docs --concurrency=2, 72/72 tasks (56 cached), VERDICT command-exit 0. One lock call ran pnpm --filter @objectstack/rest run typecheck, then vitest --project local, then vitest --project repo, joined with &&, VERDICT command-exit 0. Typecheck: the rest test layer has 0 errors. Local: 233 files, 4545 passed, 71 skipped. Repo: 1 file, 8 passed. These counts equal round 3's. Gates: dispatch-gates --commands at 2d488c8 derives 112, the same list as rounds 2 and 3. The union ran under the lock (VERDICT command-exit 0, held 19m19s), and dispatch-gates --ran, also under the lock, reads '112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED' (all 112 exit 0). CI at 2d488c8, read 2026-09-30T10:17Z and deduplicated by check name: 35 names, 33 success, 2 skipped (Console Pin Gate; Packed-tarball smoke (opt-in)), none running; the seven required contexts are all success. NOT MEASURED: acceptance 6 (a hand check).",
    "mcp_calls": "0",
    "api_writes": "1 through the fleet-write relay: this report comment (POST /repos/objectstack-ai/objectstack/dispatches, then POST /repos//issues/18386/comments). One git push, 0858d89 to 2d488c8 (not REST). REST reads: PR comment 5908288272 and check-runs.",
    "open_questions": [],
    "out_of_scope_findings": []
    }

  8. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Landed: PR #20683 → e5c7d07ed4. Acceptance 1–5 are met; acceptance 6 needs a person, so the card moves to pm:awaiting-maintainer · 2026-09-30T10:58Z

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

    • Landed: PR feat(rest): GET /data/:object/export?template=true answers an xlsx import template (#18386) #20683 merged through the merge queue at 2026-09-30T10:57Z as e5c7d07ed4, with one parent, 7afdc5c59f. The PR says Part of #18386, so the card stays open.
    • Content check: each of the 13 files has the same git patch-id --stable as the head 2d488c89ee against its merge base 7a09eee1b1.
    • Review: the at-tier PASS 5908288272 was taken at 0858d89fc3. The seat's delta ACCEPT 5909405096 extended it to 2d488c89ee, which added only the two ordered cuts.
    • Delivered:
      • GET /api/v1/data/:object/export?template=true answers the xlsx template, following the column rule as corrected by ruling 5904855243.
      • The optional ISecurityService.getWritableFields narrows the columns, and plugin-security implements it. A fallback to the read projection is stated in X-Export-Template-Projection and on the instructions sheet.
      • Without template, the export is unchanged byte for byte.
    • Acceptance 1–5 are pinned by the route and unit tests in packages/rest/src/import-template*.test.ts. Acceptance 6 is not measured: CI cannot open a spreadsheet app.
    • The objectui half: fix(plugin-grid): 「下载模板」列出全部字段(含系统/只读列),改调后端模板接口 objectui#9600 (the 「下载模板」 button calls this endpoint). It needs a framework release that carries e5c7d07ed4.

    需要一位同事手工核对验收 6

    请按下面三步做一遍,然后在本卡回复「6 通过」,或者写明哪一个软件、哪一列有问题:

    1. 启动一个带 select 和布尔字段的应用,例如 showcase。以普通业务用户身份下载 GET /api/v1/data/<对象名>/export?template=true。
    2. 分别用 Excel、WPS、Numbers、Google Sheets 打开下载的文件。检查 select、radio、布尔列能否点出下拉,下拉的选项是否与字段定义一致;「填写说明」页是否可读。
    3. 在每个软件里各填一行、保存,再通过导入功能导回。确认没有出现 invalid_option / invalid_boolean。

    核对通过后,本卡以 completed 关闭。

    Transition in this act: pm:dispatched → pm:awaiting-maintainer. The seat's assignee comes off, and the claim is released because nothing is in flight here.


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    验收 6 机器验证读数:结构和往返通过,发现一处不符合项,暂不关卡 · 2026-09-30T15:45Z

    domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1)。维护者裁定原话:「A,机器验证通过就直接关卡」。验证由本席派出的验证代理完成,没有做任何 GitHub 写入。被测树是 main 2d5fe76f43,已包含 e5c7d07ed4。复现脚本在本席 scratchpad 的 verify-18386/repro.sh。

    机器测过、通过的部分

    • 结构(OOXML): 测了 showcase_task 和 showcase_field_zoo 两个对象,各取 zh-CN、en 两份模板,共 4 份,经真实的 RestServer 路由导出,0 项失败。
      • 每个 select / radio / boolean 列都有一条 type="list" 的数据验证,sqref 覆盖第 2–50001 行。
      • formula1 引用「填写说明」页的区域,中文表名按规范加了引号;区域里的值与该语言下的选项 label 一致。
      • 两张工作表都通过 ECMA-376 sml.xsd 校验,另用两个反例确认校验器会报错。
    • 往返(真实导入接口): 改过的 zh-CN 模板以 zh-CN 请求导回,两个对象都成功落库,没有 invalid_option 或 invalid_boolean。

    没测成的部分

    • LibreOffice 打开: 容器里只有 libreoffice-core,没有 Calc,打不开 xlsx。验证代理的安装被本地权限拦下,本席没有绕过。
    • Excel、WPS、Numbers、Google Sheets: 这个容器测不到。前两个没装,也没有图形界面;Numbers 只有 macOS/iOS 版;Google Sheets 需要登录。
    • 公开资料显示:Excel 2010 及以后的版本保存跨表下拉时写在扩展命名空间里,本模板写在主命名空间里。标准允许这种写法,但 Excel 打开时是否会弹修复提示,只能用真 Excel 确认。

    不符合项(因此不关卡)

    1. * 忽略了选项级默认值。 import-template.ts:208-209 的 isTemplateRequired 只看 defaultValue,但引擎插入时会应用 options[].default: true(objectql/src/engine.ts:5662 resolveOptionDefault)。
      • 实测:showcase_task.status 是必填,选项 backlog 标了 default: true。表头被标成 状态 *,说明页写的是「留空的行会被拒绝」,但留空的行实际被接受,落库为 backlog。
      • 这违反验收 2,也让说明页上的这句话不成立。本席接着派一轮修复。

    风险(不阻塞,分别转交)
    2. showcase 里没有任何人能下载模板。 模板接口挂在导出权限闸后面,而 showcase 没有任何权限集授予 allowExport,普通用户和 admin 都返回 403。于是只有新建权限、没有导出权限的导入者拿不到模板。这是产品层面的取舍,本席另开决策卡。
    3. 本地化 label 与请求语言绑定。 zh-CN 模板以 en 请求导入,会报 invalid_option。objectui 的导入向导必须转发同一个 locale,本席会转告 objectui#9600。
    4. errorTitle 没有截断到 32 字符(Excel 界面的上限)。示例应用里最长的只有 11 字符,暂时碰不到。随第 1 项的修复一起处理。


    Generated by Claude Code

  10. 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

  11. 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."
    ]
    }

  12. 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

  13. 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

  14. 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

  15. 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