소스 정보
- 저장소
- speakeasy-api/gram
- 최근 소스 활동
- 2026년 8월 18일 19:14
- 감지된 SKILL.md 언어
- 영어
- 스타
- 267
- 포크
- 31
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/speakeasy-api/gram --skill golang명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Conventions for authoring or editing analyzers in the `glint/` Go static-analysis package — Gram's custom golangci-lint plugin built on `go/analysis`. Activate this skill whenever the task involves adding, modifying, or testing a `glint` analyzer (new rule key, new diagnostic, settings struct, fixture under `glint/testdata/`, wiring in `BuildAnalyzers`), even if the user does not say "glint" explicitly — phrases like "add a lint rule", "write a custom analyzer", "go/analysis", or "enforce X via golangci-lint" should trigger it.
Use when adding, editing, reviewing, testing, or locating a reviewed skill distributed with the Platform MCP plugin; triggers include "Platform MCP skill", "platform_mcp_skills", "add a catalog workflow", "bundle a skill with Platform MCP", and "where should this Platform skill live?".
Use when adding a new feature or changing an existing one that surfaces data in the dashboard — the demo org (app.getgram.ai/explore-demo) must show it — and when editing or extending the demo org seed or the local dev seed — server/internal/demoseed/{postgres,clickhouse}.sql, demoseed.Spec, RunLocalFixtures, demo.ensure_demo_org(), gram demo-seed, mise run seed, mise run seed:demo, seed/demo/ docs, TestDemoSeedSafety, the demo-seed-safety CI job, org_gram_demo_workspace, dec0de00 uuids — or when that CI check fails with "touched another tenant's rows", "reseed is not cleaning up or not idempotent", "still contains the demo identifier", or a demo seed pre/postflight exception.
SKILL.md 표시 중
| name | golang |
| description | Rules and best practices when writing and editing Go (Golang) code |
| metadata | {"relevant_files":["server/**/*.go","functions/**/*.go","cli/**/*.go"]} |
This codebases uses features from Go 1.25 and above.
DebugContext, InfoContext, WarnContext, ErrorContext.attr.SlogError(err). Example: logger.ErrorContext(ctx, "failed to write to database", attr.SlogError(err)).context.Context value as their first argument.go bare. Prefer a mise task when one exists; for server tests, use mise run test:server, which runs from server/ and accepts the same extra arguments as go test, e.g. mise run test:server ./internal/oops/. When no mise task exists, prefix with mise exec --, e.g. mise exec -- go test ./server/internal/oops/. A bare go can resolve to a system install (Homebrew, asdf, a distro package) whose patch version differs from the toolchain pinned in mise.toml, while GOROOT still points at the mise install. The build then fails on every stdlib package with compile: version "go1.26.4" does not match go tool version "go1.26.5". The same applies to the other pinned Go tools (golangci-lint, gotestsum, sqlc, gomigrate), and to goa, which is a go.mod tool directive and so inherits whichever toolchain invoked it.mise lint:server to run the linters on the server codebase.exhaustruct linter requires all struct fields to be explicitly set in struct literals. When adding new fields to a type, update ALL call sites — including places that construct the struct with zero values (e.g., MyStruct{} → MyStruct{NewField: nil}).Deprecated: use X instead on a symbol kept for compatibility (as its own trailing paragraph, which is the form gopls and pkg.go.dev surface to callers), build tags, //go: directives, and TODO(TICKET-123) notes. Write the rest of a deprecated symbol's doc in the present tense, describing what it still does today.Foo above", "see handler.go for details"). The target moves or gets rewritten and the pointer silently goes stale. The test is what the comment does with the name it mentions: stating the fact where the reader needs it is fine ("callers must hold Registry.mu"), sending the reader elsewhere to go find it is not ("see Registry for locking rules"). Referencing a symbol, package, ticket, or external spec that the comment is actually about is fine. Repeating a sentence or two to keep an explanation local is fine; when the full explanation is too long to restate, document it on the type or package that owns the invariant and give each use site the one-line consequence it needs.go doc <Type>.<Field> and editor hovers show nothing for the rest. Grouped const and var blocks and interface methods follow the same attachment rule, and a comment above the const ( line documents the block rather than any spec in it. Put a blank line between the previous field and the next field's comment: attachment survives without it, but the blank line keeps the boundary obvious. Never leave a blank line between a comment and the field it documents, which silently detaches it. The first field in a block needs no blank line before its comment, and gofmt flags none of these mistakes.type Config struct {
// Timeouts applied to outbound requests. Previously these were a single
// Timeout field. See the note on Client above for retry interactions.
ReadTimeout time.Duration
WriteTimeout time.Duration
}
Three problems in four lines: the comment narrates a past refactor, it defers to a comment that can move or disappear, and WriteTimeout ends up with no documentation at all.
type Config struct {
// ReadTimeout bounds how long the client waits for response headers.
// Retries each get a fresh budget, so a request can exceed this in total.
ReadTimeout time.Duration
// WriteTimeout bounds how long the client spends sending the request body.
WriteTimeout time.Duration
}
We use Goa to design our API and generate server code. All Goa code lives in server/design. The Goa DSL is documented in https://pkg.go.dev/goa.design/goa/v3/dsl.
To make an API change such as creating a new service or update an existing one:
server/design to reflect the API change.mise run gen:goa-serverserver/gen with the new API changes. It's best to use git to discover the added/changed files.When implementing Goa services:
server/internal/<service>/impl.go.package assets
import (
"context"
"log/slog"
goahttp "goa.design/goa/v3/http"
gen "github.com/speakeasy-api/gram/server/gen/assets"
srv "github.com/speakeasy-api/gram/server/gen/http/assets/server"
"github.com/speakeasy-api/gram/server/internal/auth"
)
type Service struct {
tracer trace.Tracer
logger *slog.Logger
auth *auth.Auth
// dependencies
}
func NewService(
logger *slog.Logger,
tracerProvider trace.TracerProvider,
auth *auth.Auth,
// dependencies
) *Service {
return &Service{
// initialize dependencies
}
}
var _ gen.Service = (*Service)(nil)
var _ gen.Auther = (*Service)(nil)
func Attach(mux goahttp.Muxer, service *Service) {
endpoints := gen.NewEndpoints(service)
endpoints.Use(middleware.MapErrors())
endpoints.Use(middleware.TraceMethods(service.tracer))
srv.Mount(
mux,
srv.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil),
)
}
func (s *Service) APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error) {
return s.auth.Authorize(ctx, key, schema)
}
func (s *Service) ListAssets(ctx context.Context, payload *gen.ListAssetsPayload) (*gen.ListAssetsResult, error) {
// implementation
}
If you are creating a new Goa service, then make sure to attach it to the http server in server/cmd/gram/start.go.
repo.New) when needed in functions.repo.Queries directly on a service struct for a new service.type Service struct {
queries *repo.Queries
}
func NewService(db *pgxpool.Pool) *Service {
return &Service{
queries: repo.New(db),
}
}
This makes the service depend on a concrete query helper instance up front, which is not the pattern we want for new services.
type Service struct {
db *pgxpool.Pool
}
func NewService(db *pgxpool.Pool) *Service {
return &Service{db: db}
}
func (s *Service) Handler(ctx context.Context) error {
queries := repo.New(s.db)
if err := queries.DoThing(ctx); err != nil {
return fmt.Errorf("do thing: %w", err)
}
return nil
}
This keeps the service dependency simple and avoids baking repo.Queries into the service shape.
ActiveOrganizationID is present.ActiveOrganizationID outside that boundary unless there is another concrete code path proving otherwise.Avoid patterns that treat ActiveOrganizationID as optional when reading authctx. That adds defensive code around an invariant that should already hold.
nil before calling it.deps.go based on c.String("environment").type Service struct {
client *vendor.Client
}
func NewService(cfg Config) *Service {
if cfg.APIKey == "" {
return nil
}
return &Service{client: vendor.New(cfg.APIKey)}
}
func (s *Service) Send(ctx context.Context, req *vendor.Request) error {
if s.client == nil {
return nil
}
return s.client.Send(ctx, req)
}
This leaks vendor types into internal code and spreads nil handling into runtime call paths.
type Client interface {
Send(ctx context.Context, message Message) error
}
type Message struct {
To string
Subject string
Body string
}
type Service struct {
client Client
}
func NewService(client Client) *Service {
return &Service{client: client}
}
Wire the real or stub implementation in deps.go so the service always receives a valid Client, and keep vendor-specific types inside the wrapper implementation.
Sending transactional email goes through server/internal/email. The package wraps Loops and enforces a strongly typed Template interface.
Follow the craft-transactional-emails skill. The Go integration is:
TemplateKey constant to server/internal/email/templates.go. Provider IDs never belong in application source.server/internal/email/template_<name>.go with a struct implementing Key(), Variables(), and AddToAudience().RegisteredTemplates so the application/manifest contract checks include it.manifest.json entry under server/internal/email/loops/; merge CI creates the Loops email and gram-infra supplies its environment-specific ID at runtime.To send: call s.emailSvc.Send(ctx, recipientEmail, tmpl) where tmpl is your populated template struct.
Variables() must return snake_case keys. Loops substitutes these keys directly into template variables — camelCase keys silently render as blank fields in the delivered email.
Every declared key must be present in the returned map even when the value is empty. A missing key causes partial template rendering.
func (t MyTemplate) Variables() map[string]string {
return map[string]string{
"approvalUrl": t.ApprovalURL,
"requesterEmail": t.RequesterEmail,
}
}
camelCase keys silently render as blank fields in Loops — no error, no warning.
func (t MyTemplate) Variables() map[string]string {
return map[string]string{
"approval_url": t.ApprovalURL,
"requester_email": t.RequesterEmail,
}
}
AddToAudience semanticsControls whether Loops upserts the recipient as a contact in the audience when the email is sent.
true for user-facing emails that are part of the recipient's product journey (team invites, onboarding).false for operational/admin emails where the recipient is incidental (admin alerts, system notifications).Base test setup — never pass nil for *email.Service:
loopsClient := loops.New(ctx, logger, nil, "") // nil guardian policy is safe when key is empty; returns noop client
noopEmailSvc := email.NewService(logger, loopsClient)
Asserting on sent emails — use a capture client:
loops.Client is our own interface (not a vendor type), so a hand-rolled capture client is appropriate here. The capture pattern lets tests assert on the exact payload sent — use it instead of testify/mock for Loops email assertions.
type captureLoopsClient struct {
mu sync.Mutex
sent []loops.SendTransactionalInput
}
func (c *captureLoopsClient) SendTransactional(_ context.Context, input loops.SendTransactionalInput) error {
c.mu.Lock()
defer c.mu.Unlock()
c.sent = append(c.sent, input)
return nil
}
func (c *captureLoopsClient) Sent() []loops.SendTransactionalInput {
c.mu.Lock()
defer c.mu.Unlock()
out := make([]loops.SendTransactionalInput, len(c.sent))
copy(out, c.sent)
return out
}
To use it in a test, declare an instance and swap it into the service:
captured := &captureLoopsClient{}
svc.emailSvc = email.NewService(testenv.NewLogger(t), captured)
(This assigns an unexported field — works from within the same package, which is the convention for access package tests.)
Optional display fields — use conv.Default:
DisplayName: conv.Default(request.DisplayName, "(unknown resource)"),
Never send a template with a blank field that produces broken email copy. Apply a meaningful fallback at the Go layer, not in the Loops template.
func (s *Service) listWidgets(ctx context.Context) error {
return s.repo.ListWidgets(ctx)
}
func (s *Service) List(ctx context.Context) error {
return s.listWidgets(ctx)
}
The wrapper adds no abstraction and is only used once.
func (s *Service) List(ctx context.Context) error {
return s.repo.ListWidgets(ctx)
}
In low-level functions, use fmt.Errorf to wrap errors with distinct and useful context:
func SaveUser(repo Repository, u User) error {
err := repo.Save(u)
if err != nil {
return fmt.Errorf("failed to save user: %w", err)
}
return nil
}
Do not need to use "failed to" language.
func SaveUser(repo Repository, u User) error {
err := repo.Save(u)
if err != nil {
return fmt.Errorf("run database query: %w", err)
}
return nil
}
Do not use generic language that doesn't add any context and doesn't improving searching for errors in the codebase.
func SaveUser(repo Repository, u User) error {
err := repo.Save(u)
if err != nil {
return fmt.Errorf("save user: %w", err)
}
return nil
}
This is much better. The error message is concise and to the point and unique to the call site.
In higher-level functions of the server/ codebase, which include HTTP service handlers, use the server/internal/oops package which allows us to wrap internal errors with user-facing error messages.
func (s *Service) ListDeployments(ctx context.Context, form *gen.ListDeploymentsPayload) (res *gen.ListDeploymentResult, err error) {
var cursor uuid.NullUUID
if form.Cursor != nil {
c, err := uuid.Parse(*form.Cursor)
if err != nil {
return nil, oops.E(oops.CodeBadRequest, err, "invalid cursor").LogError(ctx, s.logger)
}
cursor = uuid.NullUUID{UUID: c, Valid: true}
}
}
server/internal/attr/conventions.go when logging in the server codebase.logger.With(attr.SlogXXX(...)) to capture contextual attributes for logging in later parts of code.logger.InfoContext(ctx, "user created", "user_id", userID)
This is bad because it doesn't use the attributes from the convention package.
import "github.com/speakeasy-api/gram/functions/internal/attr"
func Example() {
logger.Error("failed to create user", attr.SlogError(err))
}
This is bad because it uses logger.Error instead of logger.ErrorContext.
import "github.com/speakeasy-api/gram/functions/internal/attr"
func Example(ctx context.Context) {
logger.ErrorContext(ctx, "failed to create user", attr.SlogError(err))
}
This is great because:
logger.ErrorContext which is the convention for logging in the server codebase.attr.SlogError attribute from the attr package.server/internal/conv)Use the conv package for common type conversions instead of writing inline helpers. Key functions:
conv.PtrEmpty(v) — If v is not the zero value, return a pointer to v; otherwise, return nil.conv.PtrValOr(ptr, default) — dereference a pointer with a fallback default.conv.Default(val, default) — return val unless it is the zero value, then return default.conv.ToPGText, conv.ToPGTextEmpty, conv.PtrToPGText, conv.PtrToPGTextEmpty — convert strings to pgtype.Text.conv.FromPGText, conv.FromPGBool — convert pgtype values to Go pointer types.conv.PtrToPGBool — convert a *bool to pgtype.Bool.conv.Ternary(cond, trueVal, falseVal) — inline conditional expression.Do NOT reimplement pointer helpers, ternary expressions, or pgtype conversions inline. Always reach for conv first.
server/internal/o11y)Use the o11y package for deferred cleanup and error logging. Two key functions:
o11y.LogDeferfunc LogDefer(ctx context.Context, logger *slog.Logger, cb func() error) error
Use LogDefer when a cleanup operation's error should be logged. Wrap cleanup calls with defer o11y.LogDefer(...) so failures are always visible in logs.
defer o11y.LogDefer(ctx, logger, func() error { return file.Close() })
o11y.NoLogDeferfunc NoLogDefer(cb func() error)
Use NoLogDefer when a cleanup operation's error can be silently discarded — for example, rolling back a database transaction (which is a no-op if the transaction already committed) or closing an HTTP response body.
dbtx, err := s.repo.DB().Begin(ctx)
if err != nil {
return nil, oops.E(oops.CodeUnexpected, err, "error accessing resource").LogError(ctx, logger)
}
defer o11y.NoLogDefer(func() error { return dbtx.Rollback(ctx) })
defer o11y.NoLogDefer(func() error { return resp.Body.Close() })
o11y.LogDefer or o11y.NoLogDefer for deferred cleanup instead of bare defer resource.Close() calls. Bare defers silently discard errors with no traceability.LogDefer when the error matters for debugging (file I/O, critical resource cleanup).NoLogDefer when the error is expected or inconsequential (transaction rollbacks, response body closes).github.com/stretchr/testify/require exclusively.time.Sleep to wait for eventual consistency or async state in tests. It is reported by the forbidigo rule GG013 (enforced repo-wide, with a small grandfathered allowlist in server/.golangci.yaml). Poll instead: require.EventuallyWithT to wait until assertions pass or require.Never to assert a condition never becomes true. Inside an EventuallyWithT closure, make assertions with assert.* against the supplied *assert.CollectT — the one sanctioned use of assert over require.testing/synctest (synctest.Test + synctest.Wait) for testing purely in-process timer/debounce logic. This is one of the few allowed time.Sleep use cases in tests since it is required for advancing the fake clock inside a synctest bubble.t.Context() instead of context.Background(), except inside t.Cleanup(func()) callbacks.t.Run to create subtests. Prefer writing separate test functions instead.setup_test.go files. Look for these across the codebase for inspiration and guidance.SELECT, INSERT, UPDATE, DELETE, transactions (Begin/BeginTx), CopyFrom, and SendBatch are all covered. Use SQLc-generated methods. Default to adding new fixture queries in the relevant domain package's own queries.sql (e.g. a toolsets-shaped fixture goes in server/internal/toolsets/queries.sql, not in testenv). Reach for server/internal/testenv/queries.sql (and testenv/testrepo) only when a fixture query is genuinely reused across multiple packages. The glint no-testing-raw-sql rule enforces this against , , , and receivers in . ClickHouse uses a different driver and is not flagged.ctx := context.Background()
This loses the test lifecycle context that Go now provides directly on *testing.T.
ctx := t.Context()
type mockEmailClient struct {
mock.Mock
}
func (m *mockEmailClient) Send(ctx context.Context, message Message) error {
args := m.Called(ctx, message)
return args.Error(0)
}
Use testify/mock when mocking integrations so expectations stay explicit and consistent across tests.
*pgxpool.Pool*pgx.Connpgx.Txpgx.Querier*_test.gogithub.com/stretchr/testify/mock for mocking third-party libraries in tests instead of ad hoc fakes around vendor types.testenv.NewLogger(t), testenv.NewTracerProvider(t), and testenv.NewMeterProvider(t) instead of constructing loggers or noop OTel providers inline. testenv.NewLogger(t) discards in normal runs and emits pretty logs under go test -v, which inline slog.New(slog.DiscardHandler) and slog.New(slog.NewTextHandler(os.Stdout, nil)) do not. Exception: tests that assert on log output should use a capturing handler over a bytes.Buffer.