feat(base): improve field update completion signals - #2219
Conversation
Co-authored-by: TRAE CLI <noreply@bytedance.com>
📝 WalkthroughWalkthroughBase field commands now accept a hidden field-name alias, validate selectors and field definitions more strictly, support explicit auto-number reformatting through the Bitable v1 endpoint, and treat matching no-op updates as successful unchanged results. ChangesBase field operations
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant BaseFieldUpdate
participant FieldOperations
participant BitableV1
BaseFieldUpdate->>FieldOperations: submit auto-number update
FieldOperations->>BitableV1: send converted reformat request
BitableV1-->>FieldOperations: return update result
FieldOperations-->>BaseFieldUpdate: return completed or unchanged response
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@bd8db4ca98c7fa554716cad9ea29baf0267a03bb🧩 Skill updatenpx skills add yballul-bytedance/cli#auto-research-sync/01KWNBD6WC5QZ2YTZZBB6CTZ37/mr-948-d8299934 -y -g |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@shortcuts/base/base_dryrun_ops_test.go`:
- Around line 88-93: Add a direct case in the dry-run field-get tests using `"
Amount "` as the `field-id-or-name` value, and assert through `dryRunFieldGet`
that the generated request path contains `/fields/Amount`. Keep the existing
untrimmed selector coverage and use the same `assertDryRunContains` pattern.
In `@shortcuts/base/base_shortcuts_test.go`:
- Around line 1208-1216: Update the BaseFieldGet validation failure assertions
to verify typed metadata, not only error text: use errors.As to extract
*errs.ValidationError and assert its Param, and assert the expected category and
subtype through errs.ProblemOf(err) where supported. Apply the same metadata
assertions to the related error-path test around the field-id alias validation.
In `@shortcuts/base/field_ops.go`:
- Around line 256-270: Update the reformat_existing_records branch in
fieldUpdate to return verification guidance requiring field readback and
sampling regenerated record values, rather than field_get_recommended:false,
field_get_required:false, and next_step:"done". Update
shortcuts/base/base_execute_test.go lines 980-1015 to assert this required
guidance, and align skills/lark-base/references/lark-base-field-update.md lines
107-115 with the resulting output contract.
- Around line 559-563: Update the rule length handling in the incremental-number
branch to distinguish an absent length from an explicitly provided one: use the
default value 3 only when length is absent, and return a validation error for
explicit values outside the documented 1..9 range, including 0 and values above
9. Preserve the existing system_number response for valid lengths.
- Around line 330-337: Update isFieldUpdateNoop in shortcuts/base/field_ops.go
(lines 330-337) to recognize the no-op solely by baseFieldUpdateNoopCode,
removing the problem.Message text check. Add a regression case in
shortcuts/base/base_execute_test.go (lines 909-929) using code 800070003 with a
different message and assert it still returns the successful no-op result.
- Around line 148-154: In shortcuts/base/field_ops.go lines 148-154, validate
containsAutoNumberReformatKey(body) before branching on
runtime.Bool("reformat-existing-records"), returning the existing typed
validation error for every embedded reformat key; only invoke
buildAutoNumberReformatBody when no embedded key is present and the flag is
enabled. In shortcuts/base/base_shortcuts_test.go lines 1254-1262, add coverage
supplying both --reformat-existing-records and an embedded reformat key, and
assert the typed validation failure.
- Around line 466-567: Replace the map-based conversion in
buildAutoNumberReformatBody with typed input models for the field definition,
property, auto_serial, style, and rules, decoding the incoming JSON at the
boundary with strict type validation. Add dedicated projection functions for the
auto_serial and style/rules shapes that produce a typed Bitable v1 request,
preserving existing defaults and validation errors. Return the projected request
in the existing API shape without direct map reads or writes in the conversion
helpers.
In `@skills/lark-base/references/lark-base-field-json.md`:
- Line 476: Update the existing-record regeneration statement in the auto-number
update documentation to clarify that values are regenerated only when
`--reformat-existing-records` is specified; standard updates use the Base v3
endpoint without setting `reformat_existing_records`.
In `@skills/lark-base/references/lark-base-field-update.md`:
- Around line 56-58: Add the text language identifier to the fenced code block
containing the PUT endpoint, changing the unlabeled fence to a text-labeled
fence while preserving the endpoint content.
In `@tests/cli_e2e/base/base_field_dryrun_test.go`:
- Around line 37-40: Update the validation assertions in the base field dry-run
test to also verify the expected invalid-argument value of error.subtype,
alongside the existing error.type and error.param checks. Keep the assertion
against the structured result.StdErr payload and preserve the current message
validation.
In `@tests/cli_e2e/base/base_field_update_dryrun_test.go`:
- Around line 37-58: Add a live E2E test alongside
TestBaseFieldUpdateAutoNumberReformatDryRun that creates a base, table,
auto-number field, and records; invokes base field-update with
--reformat-existing-records and --yes using the changed numbering rule; verifies
the updated auto-number values through the live API or CLI; and reliably cleans
up all created resources. Keep the workflow self-contained and follow existing
live E2E create/use/cleanup patterns in tests/cli_e2e.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 152b48b8-c614-4f35-918e-aef5cd61c0ea
📒 Files selected for processing (12)
shortcuts/base/base_dryrun_ops_test.goshortcuts/base/base_execute_test.goshortcuts/base/base_shortcuts_test.goshortcuts/base/field_get.goshortcuts/base/field_ops.goshortcuts/base/field_update.goshortcuts/base/helpers.goskills/lark-base/SKILL.mdskills/lark-base/references/lark-base-field-json.mdskills/lark-base/references/lark-base-field-update.mdtests/cli_e2e/base/base_field_dryrun_test.gotests/cli_e2e/base/base_field_update_dryrun_test.go
| fieldGetAliasRT := newBaseTestRuntime( | ||
| map[string]string{"base-token": "app_x", "table-id": "tbl_1", "field-id-or-name": "Amount"}, | ||
| nil, | ||
| nil, | ||
| ) | ||
| assertDryRunContains(t, dryRunFieldGet(ctx, fieldGetAliasRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/Amount") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Test selector trimming.
The contract accepts one trimmed selector value. Add a case with " Amount " and assert that the generated path ends in /Amount. The current test passes if fieldGetRef stops trimming.
As per coding guidelines, each behavior change needs a direct contract test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/base/base_dryrun_ops_test.go` around lines 88 - 93, Add a direct
case in the dry-run field-get tests using `" Amount "` as the `field-id-or-name`
value, and assert through `dryRunFieldGet` that the generated request path
contains `/fields/Amount`. Keep the existing untrimmed selector coverage and use
the same `assertDryRunContains` pattern.
Source: Coding guidelines
| if err := BaseFieldGet.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t"}, nil, nil)); err == nil || !strings.Contains(err.Error(), "--field-id is required") { | ||
| t.Fatalf("err=%v", err) | ||
| } | ||
| if err := BaseFieldGet.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "field-id-or-name": "Amount"}, nil, nil)); err != nil { | ||
| t.Fatalf("field get alias validate err=%v", err) | ||
| } | ||
| if err := BaseFieldGet.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "field-id": "fld_1", "field-id-or-name": "Amount"}, nil, nil)); err == nil || !strings.Contains(err.Error(), "mutually exclusive") { | ||
| t.Fatalf("err=%v", err) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert typed metadata for validation failures.
These new error-path tests only inspect error text. Assert errs.ProblemOf(err) category and subtype. Use errors.As with *errs.ValidationError to assert Param.
As per coding guidelines, error-path tests must assert typed metadata. Based on learnings, errs.ProblemOf does not expose Param.
Also applies to: 1257-1262
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/base/base_shortcuts_test.go` around lines 1208 - 1216, Update the
BaseFieldGet validation failure assertions to verify typed metadata, not only
error text: use errors.As to extract *errs.ValidationError and assert its Param,
and assert the expected category and subtype through errs.ProblemOf(err) where
supported. Apply the same metadata assertions to the related error-path test
around the field-id alias validation.
Sources: Coding guidelines, Learnings
| if runtime.Bool("reformat-existing-records") { | ||
| if _, err := buildAutoNumberReformatBody(body); err != nil { | ||
| return err | ||
| } | ||
| } else if containsAutoNumberReformatKey(body) { | ||
| return baseFlagErrorf("--reformat-existing-records is required for auto_number existing-record regeneration; remove reformat_existing_records from --json and pass --reformat-existing-records") | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject embedded reformat keys even when --reformat-existing-records is set.
The current else if only rejects embedded keys when the dedicated flag is absent. When both are supplied, the command accepts prohibited input. It can silently discard a nested style key or forward an auto_serial key.
shortcuts/base/field_ops.go#L148-L154: checkcontainsAutoNumberReformatKey(body)before branching onreformat-existing-records, then return the validation error for every embedded reformat key.shortcuts/base/base_shortcuts_test.go#L1254-L1262: add a case that supplies both--reformat-existing-recordsand an embedded reformat key, then assert typed validation failure.
As per coding guidelines, do not silently ignore unsupported input or unhonored options.
📍 Affects 2 files
shortcuts/base/field_ops.go#L148-L154(this comment)shortcuts/base/base_shortcuts_test.go#L1254-L1262
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/base/field_ops.go` around lines 148 - 154, In
shortcuts/base/field_ops.go lines 148-154, validate
containsAutoNumberReformatKey(body) before branching on
runtime.Bool("reformat-existing-records"), returning the existing typed
validation error for every embedded reformat key; only invoke
buildAutoNumberReformatBody when no embedded key is present and the flag is
enabled. In shortcuts/base/base_shortcuts_test.go lines 1254-1262, add coverage
supplying both --reformat-existing-records and an embedded reformat key, and
assert the typed validation failure.
Source: Coding guidelines
| if runtime.Bool("reformat-existing-records") { | ||
| body, err := buildAutoNumberReformatBody(body) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| data, err := baseV3Call(runtime, "PUT", bitableV1Path("apps", baseToken, "tables", tableIDValue, "fields", fieldRef), nil, body) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| field := interface{}(data) | ||
| if inner, ok := data["field"]; ok { | ||
| field = inner | ||
| } | ||
| runtime.Out(fieldUpdateNoReadbackResult(map[string]interface{}{"field": field, "updated": true, "reformat_existing_records": true}), nil) | ||
| return nil |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not report auto-number reformatting as fully verified.
The reformat path changes existing record values but returns field_get_recommended:false, field_get_required:false, and next_step:"done". This contradicts the documented requirement to read back and sample regenerated values.
shortcuts/base/field_ops.go#L256-L270: return a verification result that recommends field readback and record-value sampling forreformat_existing_records.shortcuts/base/base_execute_test.go#L980-L1015: assert the required verification guidance instead of asserting a completed no-readback result.skills/lark-base/references/lark-base-field-update.md#L107-L115: keep the documentation aligned with the final output contract.
📍 Affects 3 files
shortcuts/base/field_ops.go#L256-L270(this comment)shortcuts/base/base_execute_test.go#L980-L1015skills/lark-base/references/lark-base-field-update.md#L107-L115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/base/field_ops.go` around lines 256 - 270, Update the
reformat_existing_records branch in fieldUpdate to return verification guidance
requiring field readback and sampling regenerated record values, rather than
field_get_recommended:false, field_get_required:false, and next_step:"done".
Update shortcuts/base/base_execute_test.go lines 980-1015 to assert this
required guidance, and align
skills/lark-base/references/lark-base-field-update.md lines 107-115 with the
resulting output contract.
| func isFieldUpdateNoop(err error) bool { | ||
| problem, ok := errs.ProblemOf(err) | ||
| if !ok { | ||
| return false | ||
| } | ||
| return problem.Code == baseFieldUpdateNoopCode && | ||
| strings.Contains(strings.ToLower(problem.Message), "no operation produced") | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Recognize the no-op response by its API code.
800070003 is the dedicated no-op code, but recognition also depends on the English message fragment "no operation produced". A localized or revised server message converts a successful no-op into an API failure.
shortcuts/base/field_ops.go#L330-L337: matchbaseFieldUpdateNoopCodewithout inspectingproblem.Message.shortcuts/base/base_execute_test.go#L909-L929: add a regression case with code800070003and a different message that still returns the successful no-op result.
📍 Affects 2 files
shortcuts/base/field_ops.go#L330-L337(this comment)shortcuts/base/base_execute_test.go#L909-L929
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/base/field_ops.go` around lines 330 - 337, Update isFieldUpdateNoop
in shortcuts/base/field_ops.go (lines 330-337) to recognize the no-op solely by
baseFieldUpdateNoopCode, removing the problem.Message text check. Add a
regression case in shortcuts/base/base_execute_test.go (lines 909-929) using
code 800070003 with a different message and assert it still returns the
successful no-op result.
| length := toInt(rule["length"]) | ||
| if length <= 0 { | ||
| length = 3 | ||
| } | ||
| return map[string]interface{}{"type": "system_number", "value": fmt.Sprintf("%d", length)}, nil |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reject invalid explicit incremental lengths.
An explicit length: 0 becomes "3" instead of failing validation. Values above 9 also pass through, although the documented range is 1..9. This can regenerate existing record numbers with a format the user did not request.
Apply the default only when length is absent. Reject explicit values outside 1..9.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/base/field_ops.go` around lines 559 - 563, Update the rule length
handling in the incremental-number branch to distinguish an absent length from
an explicitly provided one: use the default value 3 only when length is absent,
and return a validation error for explicit values outside the documented 1..9
range, including 0 and values above 9. Preserve the existing system_number
response for valid lengths.
|
|
||
| 默认值 / 约束: | ||
| - `date_format` 可用:`yyyyMMdd`、`yyyyMM`、`yyMM`、`MMdd`、`yyyy`、`MM`、`dd` | ||
| - 开启 `+field-update --reformat-existing-records` 时优先用 `yyyyMMdd` 或 `yyyyMM`;`yyMM` 会被已有记录重排接口拒绝 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the existing-record regeneration statement.
The sentence at Line 423 says every auto-number update reapplies rules to existing values. Standard updates stay on the Base v3 endpoint and do not set reformat_existing_records. State that existing values are regenerated only with --reformat-existing-records.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-base/references/lark-base-field-json.md` at line 476, Update the
existing-record regeneration statement in the auto-number update documentation
to clarify that values are regenerated only when `--reformat-existing-records`
is specified; standard updates use the Base v3 endpoint without setting
`reformat_existing_records`.
| ``` | ||
| PUT /open-apis/bitable/v1/apps/:base_token/tables/:table_id/fields/:field_id | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language to the fenced block.
Markdownlint reports MD040 because this block has no language identifier.
Proposed fix
-```
+```text
PUT /open-apis/bitable/v1/apps/:base_token/tables/:table_id/fields/:field_id</details>
Based on static analysis, fenced code blocks must specify a language.
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 56-56: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-base/references/lark-base-field-update.md` around lines 56 - 58,
Add the text language identifier to the fenced code block containing the PUT
endpoint, changing the unlabeled fence to a text-labeled fence while preserving
the endpoint content.
Source: Linters/SAST tools
| require.Empty(t, strings.TrimSpace(result.Stdout), result.Stdout) | ||
| require.Equal(t, "validation", gjson.Get(result.Stderr, "error.type").String(), result.Stderr) | ||
| require.Equal(t, "--field-id", gjson.Get(result.Stderr, "error.param").String(), result.Stderr) | ||
| require.Contains(t, gjson.Get(result.Stderr, "error.message").String(), "--field-id is required") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the validation subtype.
The test verifies error.type and error.param but omits error.subtype. Assert the expected invalid-argument subtype so the structured validation contract cannot regress to an arbitrary validation error.
As per coding guidelines, error-path tests must assert category, subtype, and parameter metadata.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/cli_e2e/base/base_field_dryrun_test.go` around lines 37 - 40, Update
the validation assertions in the base field dry-run test to also verify the
expected invalid-argument value of error.subtype, alongside the existing
error.type and error.param checks. Keep the assertion against the structured
result.StdErr payload and preserve the current message validation.
Source: Coding guidelines
| func TestBaseFieldUpdateAutoNumberReformatDryRun(t *testing.T) { | ||
| result := runBaseDryRun(t, 0, | ||
| "base", "+field-update", | ||
| "--base-token", "app_x", | ||
| "--table-id", "tbl_x", | ||
| "--field-id", "fld_x", | ||
| "--json", `{"name":"评分","type":"number","style":{"type":"rating","icon":"star","min":0,"max":20}}`, | ||
| "--json", `{"name":"编号","type":"auto_number","style":{"rules":[{"type":"text","text":"TASK-"},{"type":"created_time","date_format":"yyyyMM"},{"type":"text","text":"-"},{"type":"incremental_number","length":4}]}}`, | ||
| "--reformat-existing-records", | ||
| "--yes", | ||
| ) | ||
|
|
||
| out := result.Stdout | ||
| require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x", gjson.Get(out, "data.api.0.url").String(), out) | ||
| require.Equal(t, "/open-apis/bitable/v1/apps/app_x/tables/tbl_x/fields/fld_x", gjson.Get(out, "data.api.0.url").String(), out) | ||
| require.Equal(t, "PUT", gjson.Get(out, "data.api.0.method").String(), out) | ||
| require.Equal(t, "评分", gjson.Get(out, "data.api.0.body.name").String(), out) | ||
| require.Equal(t, "rating", gjson.Get(out, "data.api.0.body.style.type").String(), out) | ||
| require.Equal(t, int64(20), gjson.Get(out, "data.api.0.body.style.max").Int(), out) | ||
| require.Equal(t, "编号", gjson.Get(out, "data.api.0.body.field_name").String(), out) | ||
| require.Equal(t, int64(1005), gjson.Get(out, "data.api.0.body.type").Int(), out) | ||
| require.True(t, gjson.Get(out, "data.api.0.body.property.auto_serial.reformat_existing_records").Bool(), out) | ||
| require.Equal(t, "fixed_text", gjson.Get(out, "data.api.0.body.property.auto_serial.options.0.type").String(), out) | ||
| require.Equal(t, "created_time", gjson.Get(out, "data.api.0.body.property.auto_serial.options.1.type").String(), out) | ||
| require.Equal(t, "system_number", gjson.Get(out, "data.api.0.body.property.auto_serial.options.3.type").String(), out) | ||
| require.Equal(t, "4", gjson.Get(out, "data.api.0.body.property.auto_serial.options.3.value").String(), out) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Add live E2E coverage for record reformatting.
This test only verifies the generated dry-run request. It cannot verify that the Bitable v1 endpoint accepts the payload or that existing auto-number values are regenerated correctly.
Add a self-contained live workflow that creates test data, changes the numbering rule with --reformat-existing-records, verifies affected record values, and cleans up.
As per coding guidelines, new behavior under tests/cli_e2e/**/*.go requires live E2E coverage with create, use, and cleanup steps.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/cli_e2e/base/base_field_update_dryrun_test.go` around lines 37 - 58,
Add a live E2E test alongside TestBaseFieldUpdateAutoNumberReformatDryRun that
creates a base, table, auto-number field, and records; invokes base field-update
with --reformat-existing-records and --yes using the changed numbering rule;
verifies the updated auto-number values through the live API or CLI; and
reliably cleans up all created resources. Keep the workflow self-contained and
follow existing live E2E create/use/cleanup patterns in tests/cli_e2e.
Source: Coding guidelines
Summary
Improve Base field shortcut behavior for field lookup aliases, field update no-op handling, auto-number reformatting, and rating validation.
Changes
--field-id-or-namecompatibility alias forbase +field-getwhile keeping--field-idas the public flag.--reformat-existing-recordssupport for auto-number updates, including bitable v1 request conversion and dry-run coverage.max > 10and update Base skill references for the new field-update workflow.Test Plan
git diff --checkRelated Issues
Auto research task: 01KWNBD6WC5QZ2YTZZBB6CTZ37
Summary by CodeRabbit
--reformat-existing-recordsoption.