docs(stock): finalize stock-events delivery

This commit is contained in:
2026-07-23 10:08:47 +07:00
parent 73ee9a1fa8
commit 5d1e676d72
6 changed files with 42 additions and 148 deletions
@@ -0,0 +1,42 @@
# Stock Events Command
**Date**: 2026-07-23 10:06
**Severity**: Medium
**Component**: `stock` module, command metadata, docs
**Status**: Resolved
## What Happened
We shipped a new read-only `/stock_events <ticker> [days]` command for SSI iBoard corporate actions. It defaults to 30 days, accepts 1..90, and returns chronological, Telegram-safe chunks. The important design choice was to keep this path additive and raw: the command reads SSI event data directly instead of forcing it through the strict dividend-normalization path used by `/stock_portfolio`.
That decision mattered because the review found a bad assumption in the original normalization approach: optional SSI fields like ex-right, record, or payment date can be malformed without the event itself being unusable. We stopped trying to force every event into a dividend-shaped class and exposed the raw fields instead. That is the right tradeoff for an undocumented provider.
## The Brutal Truth
This was more fragile than it needed to be. We had to correct a command-menu metadata regression after the feature landed, and the review exposed that strict parsing was silently dropping valid SSI events. That is exactly the kind of bug that wastes time because it looks “clean” until you realize you are throwing away real data.
## Technical Details
- Feature landed in `feat(stock): add stock events lookup`.
- Command registration had to be kept in sync with Telegram help/menu metadata for `<ticker> [days]`.
- Review showed the generic path should not parse raw SSI events into a rigid class.
- Verification gates passed after fixes: focused stock tests, `go test -count=1 ./...`, `go vet ./...`, `go build ./...`, `golangci-lint run`, and `git diff --check`.
## What We Tried
- Reused the SSI corporate-action fetcher.
- Initially normalized events like dividends.
- Fixed the menu metadata mismatch in the shared command contract.
- Dropped the rigid event-class mapping and kept the raw provider payload.
## Root Cause Analysis
We overfit an undocumented API to our internal dividend model. That made the feature look structured but hid malformed optional fields and risked losing valid events. The real mistake was prioritizing type shape over provider fidelity.
## Lessons Learned
When the source API is undocumented, raw field preservation beats premature normalization. Keep user-facing contracts exact, but do not invent structure that the provider does not guarantee.
## Next Steps
Monitor for SSI schema drift, keep `/stock_events` best-effort, and expand tests only around the raw fields and command contract. The owner is the stock module maintainer; no follow-up migration work is needed.
@@ -1,9 +0,0 @@
{
"decision": "PASS",
"disprovenClaims": [],
"unverifiedClaims": [
"The implementation cannot prove that every production SSI corporate-action variant remains representable because the endpoint is undocumented. The raw DTO path copies every decoded eventListCode and optional display field without a dividend-type filter (internal/modules/stock/dividend_events_ssi.go:147-180,227-263), while README.md:42-47 correctly limits the promise to eligible, best-effort SSI rows. Fixtures exercise AGM, ISS, OTHER, FALLBACK, and DIV shapes, including malformed optional dates and raw decimal fields (internal/modules/stock/dividend_events_ssi_test.go:82-144), but they are not an authoritative enumeration of SSI's live schema."
],
"missingProof": [],
"reachableRegressions": []
}
@@ -1,41 +0,0 @@
{
"skill": "ck:cook",
"mode": "interactive",
"task": "Add /stock_events <ticker> [days] for read-only SSI corporate-action lookup",
"acceptanceCriteria": [
"default lookback is 30 days and an explicit whole-number lookback from 1 through 90 is accepted",
"all eligible SSI corporate actions for the ticker are returned for the exact (after, through] interval",
"raw SSI event name, title, description, date strings, value, and ratio are preserved for display without dividend normalization",
"a malformed optional date remains displayable, a malformed non-empty publicDate is rejected, and a missing publicDate uses the first parseable optional date only as its filter/order cursor",
"SSI pagination is complete, CorId values are deduplicated, and output order is deterministic by publication time then provider ID",
"the command is senderless and read-only, with friendly usage, empty-result, and upstream-error replies",
"every Telegram reply stays below the 4000-character safety budget without reordering events",
"existing dividend normalization, portfolio discovery, retained history, and callback behavior remain unchanged",
"README, command metadata, usage text, registration tests, help/menu discovery, and server expectations remain exact"
],
"touchpoints": [
"internal/modules/stock/dividend_events_ssi.go",
"internal/modules/stock/stock_events.go",
"internal/modules/stock/handlers.go",
"internal/modules/stock/stock.go",
"internal/modules/stock/dividend_events_ssi_test.go",
"internal/modules/stock/stock_events_test.go",
"internal/modules/stock/handlers_test.go",
"cmd/server/command_menu_test.go",
"README.md"
],
"publicContracts": [
"Telegram command: /stock_events <ticker> [days]",
"default days: 30; accepted explicit range: 1..90",
"SSIStockEventProvider.FetchStockEvents(ctx, symbol, after, through)",
"DividendEventProvider.FetchDividendEvents(ctx, symbol, after, through) remains unchanged"
],
"blastRadius": [
"stock command registration and dispatch",
"Telegram native command menu and /help presentation",
"shared SSI corporate-action HTTP pagination used by stock-event and dividend discovery",
"stock portfolio dividend notification discovery",
"user-facing README contract"
],
"scoutSummary": "The re-review traced raw SSI fields through pagination, copyStockEvent, cursor selection, formatting, chunking, module registration, help/menu metadata, server discovery, and the unchanged strict dividend caller. Generic events retain malformed optional date strings for display. Non-empty publicDate must parse and cannot fall back; when publicDate is absent, the first parseable optional date is used only for exact interval filtering and deterministic ordering. The command remains senderless, store-free, and concurrency-safe, and no reachable dividend or portfolio regression was found."
}
@@ -1,24 +0,0 @@
{
"decision": "PASS",
"score": 9.6,
"criticalCount": 0,
"acceptanceCoverage": [
"default 30-day and explicit 1..90 parsing are covered by stock_events_test.go",
"exact (after, through] filtering, pagination, CorId deduplication, and deterministic ordering are covered by dividend_events_ssi_test.go",
"senderless execution, empty results, upstream failures, registration metadata, and bounded reply chunking are covered by stock_events_test.go",
"native menu metadata is covered by cmd/server/command_menu_test.go",
"README and handler usage match /stock_events <ticker> [days]",
"raw name, title, description, dates, value, and ratio are copied and rendered without dividend normalization",
"provider fixtures prove malformed optional dates remain raw/displayable, malformed non-empty publicDate is rejected, and missing publicDate uses a parseable optional cursor"
],
"regressionProof": [
"the shared FetchDividendEvents contract remains signature-compatible and the strict normalizeEvent plus parseSSIDividendDates block is byte-for-byte unchanged from HEAD",
"the generic provider test verifies non-dividend rows do not leak into dividend discovery",
"focused stock and server tests passed",
"the full repository test suite passed",
"the stock package race test passed",
"vet, build, lint, and diff checks passed"
],
"contractStatus": "CHANGED",
"blockingReasons": []
}
@@ -1,10 +0,0 @@
{
"highRisk": true,
"reasons": [
"an intentional new public Telegram command contract was added",
"the implementation refactors SSI pagination shared with existing dividend discovery"
],
"autoStopRequired": true,
"humanApproved": true,
"largeDiff": true
}
@@ -1,64 +0,0 @@
{
"commands": [
{
"command": "go test -count=1 ./internal/modules/stock ./cmd/server",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "Focused stock-module and server command-discovery tests passed."
},
{
"command": "go test -count=1 ./...",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "The complete Go test suite passed with no package failures."
},
{
"command": "go test -race -count=1 ./internal/modules/stock",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "The stock package passed under the Go race detector."
},
{
"command": "go vet ./...",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "Go vet completed without findings."
},
{
"command": "go build ./...",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "All Go packages built successfully."
},
{
"command": "golangci-lint run",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "golangci-lint reported zero issues."
},
{
"command": "git diff --check",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "No whitespace errors were found; Git emitted only line-ending conversion notices."
},
{
"command": "go test -count=1 -cover ./internal/modules/stock",
"status": "pass",
"exitCode": 0,
"timestamp": "2026-07-23T02:54:30.893Z",
"summary": "The stock package passed with 80.7% statement coverage."
}
],
"beforeAfter": {
"before": "No public stock corporate-action lookup command existed.",
"after": "The command, metadata, raw SSI provider path, tests, and README are present; malformed optional dates remain displayable, cursor validation is isolated from raw output, and all automated gates pass."
}
}