Skip to content

feat(rest): GET /data/:object/export?template=true answers an xlsx import template (#18386) - #20683

Merged
os-justin merged 9 commits into
mainfrom
claude/issue-18386-export-import-template
Sep 30, 2026
Merged

os-justin merged 9 commits into
mainfrom
claude/issue-18386-export-import-template

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Part of #18386. Acceptance 6 (dropdowns in Excel, WPS, Numbers and Google Sheets) needs a person to open the file, so the card stays open for that check after this lands. The objectui half is objectstack-ai/objectui#9600.

Clause-②: yes (widening)

GET /api/v1/data/:object/export?template=true answers an xlsx import template with no data rows. Its columns leave out system, readonly, formula, summary and autonumber fields, a writable hidden field is kept, and field-level security narrows the rest to what the caller may edit. Required columns with no default carry *, select, radio and boolean columns get dropdowns, and an instructions sheet states the spellings the import reader accepts. With no template parameter the export is unchanged, byte for byte.

ISecurityService gains an optional getWritableFields, which plugin-security implements from the write gate's own field mask. A security service without it gets the read projection instead, and the response says so in the X-Export-Template-Projection header and on the instructions sheet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-security, @objectstack/rest, @objectstack/spec, touching 74 documentable anchor(s).

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

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

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

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

Which tree this was computed on

This run read content/docs from 5f7afd68e0402774df33057c541869ded46f8ddd — the merge of head 2d488c89ee76d11cbbd2f22508fb1cb9c6badcfd into base 810d42b69cc5f581a6f3d089e02d8c5fce2ece37, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5f7afd68e0402774df33057c541869ded46f8ddd && git checkout 5f7afd68e0402774df33057c541869ded46f8ddd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 810d42b69cc5f581a6f3d089e02d8c5fce2ece37 2d488c89ee76d11cbbd2f22508fb1cb9c6badcfd && git checkout -B drift-repro 810d42b69cc5f581a6f3d089e02d8c5fce2ece37 && git merge --no-ff 2d488c89ee76d11cbbd2f22508fb1cb9c6badcfd

node scripts/docs-audit/affected-docs.mjs --json 810d42b69cc5f581a6f3d089e02d8c5fce2ece37

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
This was referenced Sep 29, 2026
…mplate narrows by it (wip)

ISecurityService gains the optional write-side twin of getReadableFields;
plugin-security answers it from the derivation its read projection and its
step 2.5 write gate share. The template asks it first, falls back to the read
projection when the service lacks it and states that in the response
(X-Export-Template-Projection and an instructions-sheet note), and no longer
excludes a writable hidden field.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
… id, as the read one does

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
…lver both projections now share

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
…urity only, and the template's formula row names its driver

Text only. The contract, both new changesets and the plugin method say the
answer is field-level security alone and a field's own rules are not in it.
The template docblock names the driver its import-door table was measured on
and says the formula refusal is the SQL driver's. The stale Object.assign
comment loses its claim that getMetadataReadableFields has no contract seat.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0858d89fc37acb469ecef63984f9602739d5490d
Local-runs: none

Inputs read: card #18386 (body as corrected under the Ruled line, all 14 comments: rulings 5896083518, 5904855243, 5906743185, the surface note 5905385065, the cross-lane note 5906517397, the triage note 5906893401, the three os-dev-reports 5894515898, 5906695622, 5908060320); PR #20683 body and file list (13 files); the diff origin/main...0858d89f (base 7a09eee1b1, 13 files, +2230/-37); the 39 check-runs on the head. Sources on main read for the judgments: security-plugin.ts step 2.5 and the posture gate, field-masker.ts, registry.ts injection flags, rule-validator.ts (stripReadonlyFields, stripRuntimeOwnedFields), the export route's fieldsFromSchema rule, export.zod.ts, packages/spec/api-surface.

① Derived judgments

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

  1. GET /data/:object/export accepts one more query parameter, template (DATA_EXPORT_PARAMS grows from ten to eleven; template also joins the repeated-parameter refusal list). true in any letter case answers the template; false or absent answers the export; any other value answers 400 VALIDATION_ERROR rather than being read as false. RIGHT: the card asks for exactly this door (「?template=true 需要进 DATA_EXPORT_PARAMS」), recognition still runs before multiplicity before mode, so every refusal the door gave before is given first and unchanged; rest-server-closed-query-params.test.ts moves its loop to eleven names with template sent alone (a csv baseline beside it is a different 400).
  2. On a template request limit, page, filter, search, searchFields, orderby, header are refused 400, and format other than xlsx is refused 400. RIGHT: the template has no rows and always carries its header, and this route's stance is that an ignored parameter is refused, never dropped.
  3. The column rule (TEMPLATE_COLUMN_EXCLUSIONS, four rows: system, readonly, computed = formula or summary, autonumber; hidden is not a row; declaration order kept; permitted intersected; ?fields= verbatim; zero data rows). RIGHT against the card's table as corrected by ruling 5904855243 (Q1 A, Q2 C), row for row. Each exclusion maps to a write-path behaviour I read on main: stripReadonlyFields (engine.ts) drops a readonly value; stripRuntimeOwnedFields (rule-validator.ts) drops an autonumber; the seven injected columns are declared system: true in registry.ts (the audit family also readonly, organization_id and owning_business_unit_id also hidden), and owner_id is refused at step 3.5 for a non-self owner without the transfer grant. hidden kept: the route test imports a hidden: true writable field through the real import door and asserts it stored.
  4. An explicit ?fields= list is used as sent, no rule and no projection narrows it, and the response header reads X-Export-Template-Projection: none. RIGHT per the card (「照办,不收窄」) and in parity with the export door's own rule on main (fieldsFromSchema = false leaves an explicit list untouched, values masked). A caller can name a field it may not read and receive its label in a header; the export on main already answers that header, so no new disclosure class opens here, and the card ruled the row.
  5. ISecurityService.getWritableFields (optional) and its plugin-security implementation, claimed to be the exact complement of what step 2.5 refuses. RIGHT, read side by side with main: step 2.5 runs for insert and update when permissionSets.length is positive, computes getFieldPermissions, folds requiredPermissions (ADR-0066 D3), intersects the delegator's mask (ADR-0090 D10) with intersectFieldMasks, then detectForbiddenWrites, whose offender set is exactly FieldMasker.getNonEditableFields(fieldPerms) (every entry with editable: false). resolveProjectionFieldMask takes the same steps in the same order with fallbackOnEmptySets: false (the same option getReadableFields passes), and getWritableFields answers allFields minus getNonEditableFields. Edges: isSystem gives all (the middleware short-circuits); no permission sets gives all (the middleware skips 2.5); an unresolvable object gives undefined (no answer); a dangling delegator gives [] (the middleware refuses the write at delegator resolution); secMeta.unresolved gives [], where the middleware refuses the whole write at the posture gate ahead of 2.5, so "no field is writable" is the true answer though the refusal there is the posture gate's, not 2.5's own. get-writable-fields.test.ts drives the real registered middleware field by field in five permission shapes and requires admission to equal membership.
  6. getReadableFields and getMetadataReadableFields now share resolveProjectionFieldMask. Answers unchanged: the same steps in the same order, the same filter (readable !== false or a partial-mask rule), the partial rules computed lazily from the same five inputs. RIGHT.
  7. The fallback-is-stated rule. resolveTemplateProjection asks getWritableFields first; a service without it, or one answering undefined, narrows by getReadableFields and the answer is marked readable; a present service answering neither is unanswered and the route refuses 500 INTERNAL_ERROR instead of an unnarrowed header. The fallback is stated twice: the X-Export-Template-Projection: readable header and a third note on the instructions sheet (English and Chinese). RIGHT: the ruling's "stated in the response, never a silent widening" is met on both carriers; the 500 is the accepted deviation from round 2. [] from getWritableFields is a real answer (no columns), never a fallback, as the contract's two-empty-answers rule says.
  8. The template's response surface: Content-Type xlsx, Content-Disposition objectName-template-YYYYMMDD-HHMMSS.xlsx with a localized label suffix, X-Export-Format: xlsx, X-Export-Template: true, X-Export-Template-Projection, Cache-Control: no-store; 404 OBJECT_NOT_FOUND when the schema cannot be resolved (the export is best-effort there; a template without a schema has no columns). RIGHT.
  9. The template runs behind the same two gates: enforceApiAccess('export') (405) then enforceExportPermission (403), before the query gates and the mode switch. RIGHT; both are pinned in the route test.
  10. The required mark * goes only on a required field with no defaultValue (seat ruling 5896083518 Q3 A). RIGHT against the engine, which fills the default before the required check. The card's acceptance 2 still reads the broader 「必填列表头带 *」 (flagged in ③).
  11. The workbook: sheet one is header plus one example row (frozen header, text-formatted columns for leading zeros); sheet two carries the notes, a five-column table (column, field, type, required, how to fill) and the dropdown source ranges; list validations cover rows 2 to 50001 through Worksheet.dataValidations, a member exceljs 4.4.0 types out, reached by one assertion that throws loudly if the runtime drops it; errorStyle: 'warning' because the reader also takes an option code and every boolean token. RIGHT against the card's structure (sheet two doubles as the dropdown source; the 255-character inline cap).
  12. The instructions sheet is built from TEMPLATE_READER_CLAIMS and the spec's IMPORT_BOOLEAN_TRUE_TOKENS / IMPORT_BOOLEAN_FALSE_TOKENS, and the tests run every quoted spelling through import-coerce.ts's own parsers. The number sentence carries the thousands-group rule the domain:cli pointer 5879289195 asked for (1 to 3 digits, then groups of exactly 3, only before the point; the decimal comma refused). RIGHT.
  13. Locale: zh* answers Chinese, anything else English, read from ?locale= and Accept-Language through the server's one locale seam. RIGHT.
  14. packages/spec/api-surface does not track interface members (the existing optional getMetadataReadableFields has no row there), so an optional member adds nothing to regenerate; check:generated is inside the green lint gate. RIGHT.
  15. content/docs/permissions/system-context.mdx row 4: anchor moved to #resolveProjectionFieldMask, text now names both projections. RIGHT; the census gate is green.

Text the diff adds or changes, tested sentence by sentence; the ones that are false, unsourced or over-broad:

  • PR body, "Its columns are the fields an import stores: system, readonly, ... are left out." OVER-BROAD. The dev cut this exact phrase from the rest changeset in round 3 (item 1c) for a measured reason: a non-readonly system field (owner_id) IS stored by the import and refused only for a non-self owner without the transfer grant, and a formula column on the memory driver creates the row. The changeset now says "Every field of the object except ..."; the body should say the same. The body is the seat's and ships nowhere; a one-line edit.
  • PR body, "Without template=true the export is unchanged, byte for byte." and rest changeset, "template=false, or no template parameter, answers the export exactly as before, byte for byte." OVER-BROAD for template=false: on main that request is refused 400 as an unknown parameter, so it has no "before". What holds, and the route test pins, is that the absent-parameter export is byte-identical to base and template=false is byte-identical to that. True on main when this lands.
  • Rest changeset, "none when no field-level security applies." Code answers none in two cases: no security service composed, and an explicit ?fields=. The second is "no projection was applied, by the card's rule", not "no field-level security exists". Harmless, since the explicit path never narrows; the exact reading is the one above.
  • Rest changeset, "The seven columns the platform adds to every object (...) are never template columns." True wherever they are injected (all seven carry system: true; the route test runs on an object that has all seven). For systemFields: false, managedBy and sys_ objects fewer are added (the dev's own note), and "never" holds vacuously.
  • import-template.ts docblock, computed row: "formula: SQL refuses the row, memory creates it." True on main today, on both drivers, as 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's dev measured (5906449929), and stated with its driver as ruling 5906743185 asked. It becomes true on no driver once the engine child engine: a caller-supplied formula value reaches the driver (SQL fails with its own text, memory stores it) — strip it, report it in droppedFields, and let engine.validate run the same write doors (engine half of #20701) #20805 lands (an open issue, no PR yet): the value will then be dropped and reported in droppedFields. That PR moves the behaviour, so that PR re-cuts the row.
  • security-service.ts, "A system context bypasses and yields the full field set." True of plugin-security; stated as contract for every implementer, exactly as the read twin's existing text does.
  • Spec changeset, "When the method is missing, the consumer may narrow by getReadableFields instead, and must say in its response that it did." Sourced: ruling 5904855243, execution parameters.
  • Plugin-security changeset, "A field is in the answer exactly when a write naming it passes the field-level-security check." True, with the posture-gate nuance in item 5.
  • Every other added or changed sentence (the DATA_EXPORT_PARAMS doc, the route's parameter list, answerImportTemplate's doc, the contract's fail-soft bullet, the log line, the Object.assign comment cut, the instructions notes) reads true against the tree at this head.
  • Unchanged text the card itself quoted as the defect: rest-server.ts:9606, "so an empty export doubles as an import template". Not touched by the diff, and now wrong in the card's own terms (the empty export's header is the read projection, system and readonly columns included; the template is the mode two lines below). Flagged in ③.

② Semver level

  • @objectstack/rest: minor, Clause-②: yes (widening). One new query-parameter value and one new response shape on an existing door; nothing removed; every request the door accepted before answers as before. RIGHT.
  • @objectstack/spec: minor, Clause-②: yes (widening). An OPTIONAL member on a public contract; an implementation that omits it still satisfies the type (the @ts-expect-error pin in security-service.test.ts), consumers feature-detect. Not breaking. RIGHT.
  • @objectstack/plugin-security: minor. A new method on the registered security service; existing answers unchanged. RIGHT. It carries no Clause line: the line's carrier is the PR body, and only a breaking changeset must repeat it.
  • PR body Clause-②: yes (widening) at line start. The diff adds a key to an accept-set and a member to a public contract, so widening is the right arm; yes takes at least minor, and all three changesets are minor. No removal or rename, so no ADR-0087 disposition is owed; the registration gate runs inside the green lint gate. The three changesets match what the diff publishes.

③ Boundary flags

Dev report 1 (5894515898):

Dev report 2 (5906695622):

  • open question, who writes the PR body: seat A with one change (5906743185). Answered; the body still carries the over-broad "fields an import stores" sentence (①).
  • deviations: 500 for a present service answering neither projection, accepted (5906743185); the system-context.mdx anchor move, accepted under the claim's pins clause. Answered.
  • out_of_scope: the Object.assign comment drift; cut in round 3, verified in the diff. Answered.

Dev report 3 (5908060320): no open questions, no findings. Its four extra cuts (1c, 2d, 2e, 3b) are deletions or word swaps on the same claims, in files already on the surface; within the seat's round-3 scope.

Cross-lane note 5906517397: item 1 (the computed row holds on SQL only) is rewritten with its driver; item 2 (getWritableFields is FLS-only) is now said by the contract, both changesets, the plugin doc and the template's resolver doc. Both answered.

Triage note 5906893401: #20805 will make the formula row true on no driver; the moment is recorded in ① and the re-cut belongs to the PR that moves the behaviour.

Concurrency: PR #20794 edits security-plugin.ts in other hunks (5905385065); this PR reads mergeable_state: clean at review time.

ESCALATED, no behaviour at stake: rest-server.ts:9606 still says "an empty export doubles as an import template", the sentence the card named as the defect. A one-line cut on this PR or a docs-only follow-up.

Acceptance 6 (dropdowns in Excel, WPS, Numbers, Google Sheets) is a hand check and stays open on the card by design; the PR is Part of #18386.

Check-runs on 0858d89f, read last, 39 runs deduped to 35 names keeping the newest started_at: 31 success, 4 skipped (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, none running. Success: Build Core, Build Docs, Check Changeset, Check Documentation Links, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, Lint and Repo Gates, 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, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core and its six shards, The card this PR closes must claim this branch, Type Check consumer gates, Type Check debt ledger, Type Check source gates, Type Check workspace, TypeScript Type Check, filter.

Implemented-by: claude/issue-18386-export-import-template
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-30T09:26Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting.


Generated by Claude Code

… longer the import template

Text only. The rest changeset keeps the byte-for-byte claim for the request
with no template parameter; template=false was refused on main, so it only
says what it answers now. The export route's comment points at template=true
instead of calling an empty export an import template.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (delta) — PR #20683 at head 2d488c89ee · domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-30T10:29Z

This note extends the at-tier PASS 5908288272 at 0858d89fc3 to 2d488c89ee. ⛔ It is not a new review.

  • Mechanical delta check, run by the seat: 0858d89fc3..2d488c89ee is one commit with a single parent. It touches exactly two files, with one hunk each, +3/−3, and these are the two cuts the record's adoption lines ordered:
    • .changeset/18386-export-import-template.md: "template=false, or no template parameter, answers the export exactly as before" becomes "template=false answers the export. Without a template parameter the export is exactly as before, byte for byte." template=false no longer claims a "before". The route test pins template=false as the export (the dev's report on the card).
    • packages/rest/src/rest-server.ts:9606: "so an empty export doubles as an import template" becomes "The import template is template=true, not this."
    • No other line moves. Behaviour and tests are unchanged.
  • CI at 2d488c89ee: 35 names after dedupe; 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke), none running. git merge-tree against origin/main is clean.
  • Not governed: check-governed-merges --pr 20683, 0 paths hit the register. The PR goes ready and into the merge queue in the next act.
  • After landing: the card stays open for acceptance 6 alone, a hand check in Excel, WPS, Numbers and Google Sheets, and moves to pm:awaiting-maintainer with a request for that check.

Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 30, 2026 10:29
@os-justin
os-justin enabled auto-merge September 30, 2026 10:30
@os-justin
os-justin added this pull request to the merge queue Sep 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2d488c89ee76d11cbbd2f22508fb1cb9c6badcfd
Local-runs: none

Director seat (objectstack#12708, summon #30 续 2), on the standing ask 「契约复审」 and the audit rule that a record names one head. The domain:spec seat 5's PASS 5908288272 names 0858d89fc3; the head then moved by one commit (2d488c89ee, single parent) that the seat covered with an "ACCEPT (delta)" note 5909405096, which says of itself "not a new review". One of the two files that commit touches is a review face (the .changeset entry), so this record carries the seat's judgments to the head that lands. Inputs: the seat's record and its inputs, the delta diff 0858d89fc3..2d488c89ee read in full (2 files, +3 / −3, one hunk each), and the check-runs on the head. git merge-tree --write-tree origin/main against the head is clean.

① Derived judgments

Every judgment of 5908288272 stands: no source line outside one comment moved, so the accept-set widening it judged (the template query parameter on the export door; the optional ISecurityService.getWritableFields; plugin-security's implementation from the write gate's own mask; the docs anchor move in system-context.mdx) is unchanged at this head.

The delta, sentence by sentence:

  1. .changeset/18386-export-import-template.md — "template=false, or no template parameter, answers the export exactly as before" becomes two sentences: "template=false answers the export. Without a template parameter the export is exactly as before, byte for byte." The old sentence claimed a "before" for a value that did not exist before this PR; the new one states each case on its own. The route test the seat cites pins template=false as the export. Right, and the seat's own adoption line ordered exactly this cut.
  2. packages/rest/src/rest-server.ts (a comment): "so an empty export doubles as an import template" becomes "The import template is template=true, not this." True at this head — the template is the template=true answer, not an empty export. Comment only.

② Semver level

Unchanged: Clause-②: yes (widening); minor on @objectstack/rest, @objectstack/spec and @objectstack/plugin-security, as the three entries declare.

③ Boundary flags

  • Ready (not draft), mergeable_state: clean, no auto-merge armed at read time; the seat's note says it enqueues in its next act. This record is written so the landing head carries a same-form PASS; if the PR lands first, it stands as the post-hoc record at that head.
  • The card stays open for acceptance 6 (a hand check of the dropdowns in Excel, WPS, Numbers and Google Sheets) after landing, per the PR's Part of first line.

Check-runs on the head, read 2026-09-30T10:43Z — none in_progress, none red.

Implemented-by: claude/issue-18386-export-import-template
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

Merged via the queue into main with commit e5c7d07 Sep 30, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18386-export-import-template branch September 30, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants