From 1dfdc8d7bf6fecd424aacaabff911475588d003b Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Wed, 15 Jul 2026 14:10:07 -0700 Subject: [PATCH] fix(security): remove blanket gosec G101 exclusion; fix findings without new exceptions Drop the `.golangci.yml` path exclusion that blanket-suppressed gosec G101 ("potential hardcoded credentials") across internal/config/embed/text/, and remove the three remaining inline `//nolint:gosec // G101` suppressions. Every finding the removal surfaces is fixed at the source rather than re-suppressed: identifiers carrying a credential-keyword substring ("token", "pass") are renamed off the trigger while their values, i18n keys, and env-var names stay intact (so no linkage or behavior changes). Findings fixed: - err.hub GenerateToken / AdminTokenRequired -> GenerateFailed / AdminRequired - env CtxTokenBudget / HubAdminToken -> CtxBudget / HubAdmin (+2 nolints) - flag DescKeyHubRevokeToken -> DescKeyHubRevokeAuth - kb/cli TokenTopicSlug -> TopicSlugPlaceholder - handover FoldEntryPassModePrefix -> FoldEntryModeAttrPrefix - err.kb *WriteRow -> *AppendRow (clears a G101 entropy false-positive and matches the open-for-append semantics) Accompanying working-tree hygiene rides along: docstring enrichment across several packages and a TraceFile -> Trace redundant-prefix rename. Verified: golangci-lint v2.11.4 reports 0 issues; go test ./... green; lint-drift clean; zero G101 suppressions remain anywhere in the tree. Signed-off-by: Jose Alekhinne --- .golangci.yml | 4 --- hack/lint-docstrings.sh | 12 ++++++++ internal/assets/commands/text/errors.yaml | 16 +++++------ internal/bootstrap/cmd.go | 4 +-- internal/cli/dream/core/doc.go | 4 ++- internal/cli/dream/core/paths/doc.go | 4 ++- internal/cli/hub/cmd/revoke/cmd.go | 4 +-- internal/cli/kb/core/topic/template.go | 2 +- internal/cli/pad/core/tag/types.go | 4 +++ internal/cli/trace/core/file/file.go | 2 +- internal/cli/trace/core/show/types.go | 13 +++++++++ internal/config/embed/flag/hub.go | 5 ++-- internal/config/embed/text/err_hub.go | 8 +++--- .../config/embed/text/err_kb_artifacts.go | 28 +++++++++---------- internal/config/embed/text/err_kb_evidence.go | 4 +-- internal/config/env/doc.go | 2 +- internal/config/env/env.go | 10 +++---- internal/config/handover/handover.go | 6 ++-- internal/config/kb/cli/cli.go | 5 ++-- internal/config/sanitize/doc.go | 2 ++ internal/ctxctl/cli/audit/cmd/list/doc.go | 4 ++- internal/entity/closeout.go | 13 +++++++++ internal/entity/kb_row.go | 9 ++++++ internal/entity/trigger.go | 4 +++ internal/err/hub/hub.go | 4 +-- .../err/kb/contradiction/contradiction.go | 2 +- internal/err/kb/decision/decision.go | 2 +- internal/err/kb/evidence/evidence.go | 2 +- internal/err/kb/glossary/glossary.go | 2 +- internal/err/kb/question/question.go | 2 +- internal/err/kb/relationship/relationship.go | 2 +- internal/err/kb/sourcemap/sourcemap.go | 2 +- internal/err/kb/timeline/timeline.go | 2 +- internal/gitmeta/types.go | 4 +++ internal/mcp/server/dispatch/poll/loop.go | 13 +++++++-- internal/mcp/server/dispatch/poll/poll.go | 10 +++++-- internal/rc/load.go | 2 +- internal/rc/rc_test.go | 6 ++-- internal/trace/types.go | 23 +++++++++++++++ internal/write/handover/markdown.go | 2 +- internal/write/handover/types.go | 11 ++++++++ 41 files changed, 182 insertions(+), 78 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 78eaf855f..d0cca7e1e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,10 +34,6 @@ linters: - linters: [gosec] text: "G30[16]" path: "_test\\.go" - # TextDescKey constants are i18n keys, not credentials - - linters: [gosec] - text: "G101" - path: "internal/config/embed/text/" run: timeout: 5m diff --git a/hack/lint-docstrings.sh b/hack/lint-docstrings.sh index e974c83c5..ef72cd470 100755 --- a/hack/lint-docstrings.sh +++ b/hack/lint-docstrings.sh @@ -26,6 +26,12 @@ set -euo pipefail +# Collect every violation first, then fail if any were found. Without +# this the checks would print findings but still exit 0, letting +# docstring regressions slip silently through `make audit`. The +# `{ ...; } || true` wrapper keeps a mid-scan non-zero from aborting +# the collection so the report stays complete. +violations="$({ find internal/ cmd/ -name '*.go' ! -name '*_test.go' ! -name 'doc.go' | sort | while read -r file; do grep -n '^func [a-zA-Z]' "$file" | while IFS=: read -r lineno rest; do funcname=$(echo "$rest" | sed 's/^func \([A-Za-z0-9_]*\).*/\1/') @@ -152,3 +158,9 @@ find internal/ cmd/ -name 'doc.go' | sort | while read -r file; do echo "SHALLOW_DOC $file ($doclines doc lines, package $pkg)" fi done 2>/dev/null +} || true)" + +if [ -n "$violations" ]; then + printf '%s\n' "$violations" + exit 1 +fi diff --git a/internal/assets/commands/text/errors.yaml b/internal/assets/commands/text/errors.yaml index 80af51095..d865ff629 100644 --- a/internal/assets/commands/text/errors.yaml +++ b/internal/assets/commands/text/errors.yaml @@ -756,7 +756,7 @@ err.kb.evidence.mkdir-dir: short: 'mkdir evidence-index parent: %w' err.kb.evidence.open-index: short: 'open evidence-index: %w' -err.kb.evidence.write-row: +err.kb.evidence.append-row: short: 'write evidence row: %w' err.kb.evidence.parse-id-number: short: 'parse EV number %q: %w' @@ -784,7 +784,7 @@ err.kb.glossary.mkdir-dir: short: 'mkdir glossary parent: %w' err.kb.glossary.open-file: short: 'open glossary: %w' -err.kb.glossary.write-row: +err.kb.glossary.append-row: short: 'write glossary row: %w' err.kb.timeline.read-file: short: 'stat timeline: %w' @@ -792,7 +792,7 @@ err.kb.timeline.mkdir-dir: short: 'mkdir timeline parent: %w' err.kb.timeline.open-file: short: 'open timeline: %w' -err.kb.timeline.write-row: +err.kb.timeline.append-row: short: 'write timeline row: %w' err.kb.sourcemap.read-file: short: 'stat source-map: %w' @@ -800,7 +800,7 @@ err.kb.sourcemap.mkdir-dir: short: 'mkdir source-map parent: %w' err.kb.sourcemap.open-file: short: 'open source-map: %w' -err.kb.sourcemap.write-row: +err.kb.sourcemap.append-row: short: 'write source-map row: %w' err.kb.relationship.read-file: short: 'stat relationship-map: %w' @@ -808,7 +808,7 @@ err.kb.relationship.mkdir-dir: short: 'mkdir relationship-map parent: %w' err.kb.relationship.open-file: short: 'open relationship-map: %w' -err.kb.relationship.write-row: +err.kb.relationship.append-row: short: 'write relationship-map row: %w' err.kb.contradiction.read-file: short: 'read contradictions: %w' @@ -816,7 +816,7 @@ err.kb.contradiction.mkdir-dir: short: 'mkdir contradictions parent: %w' err.kb.contradiction.open-file: short: 'open contradictions: %w' -err.kb.contradiction.write-row: +err.kb.contradiction.append-row: short: 'write contradictions row: %w' err.kb.contradiction.parse-c-number: short: 'parse C number %q: %w' @@ -826,7 +826,7 @@ err.kb.decision.mkdir-dir: short: 'mkdir decisions parent: %w' err.kb.decision.open-file: short: 'open decisions: %w' -err.kb.decision.write-row: +err.kb.decision.append-row: short: 'write decisions row: %w' err.kb.decision.parse-dd-number: short: 'parse DD number %q: %w' @@ -836,7 +836,7 @@ err.kb.question.mkdir-dir: short: 'mkdir questions parent: %w' err.kb.question.open-file: short: 'open questions: %w' -err.kb.question.write-row: +err.kb.question.append-row: short: 'write questions row: %w' err.kb.question.parse-q-number: short: 'parse Q number %q: %w' diff --git a/internal/bootstrap/cmd.go b/internal/bootstrap/cmd.go index f6a47b7b0..4f5e662ae 100644 --- a/internal/bootstrap/cmd.go +++ b/internal/bootstrap/cmd.go @@ -28,7 +28,7 @@ import ( writeBootstrap "github.com/ActiveMemory/ctx/internal/write/bootstrap" ) -// version is set at build time via ldflags: +// version is set at build time via `ldflags`: // // -X github.com/ActiveMemory/ctx/internal/bootstrap.version=$(cat VERSION) var version = cfgBootstrap.DefaultVersion @@ -49,7 +49,7 @@ func RootCmd() *cobra.Command { Long: long, Example: desc.Example(cmd.DescKeyCtx), Version: version, - // Cobra auto-prints returned errors to stderr by default; + // Cobra auto-prints returned errors to standard error by default; // main.go also prints them via writeErr.With, producing a // double-printed error. Silence cobra's path so writeErr is // the sole printer. (SilenceUsage stays per-return so diff --git a/internal/cli/dream/core/doc.go b/internal/cli/dream/core/doc.go index f31f0acd1..b1e2693b1 100644 --- a/internal/cli/dream/core/doc.go +++ b/internal/cli/dream/core/doc.go @@ -7,5 +7,7 @@ // Package core groups the ctx dream command's domain logic into // focused subpackages: paths (root/notebook resolution), pass (one // executor-agnostic run), and dispose (loading a proposal by id and -// applying accept/reject/amend through the engine). +// applying accept/reject/amend through the engine). The command +// itself stays a thin Cobra wrapper; all triage behavior lives here +// so it can be exercised without the CLI layer. package core diff --git a/internal/cli/dream/core/paths/doc.go b/internal/cli/dream/core/paths/doc.go index 413c3f74e..9c41c3661 100644 --- a/internal/cli/dream/core/paths/doc.go +++ b/internal/cli/dream/core/paths/doc.go @@ -7,5 +7,7 @@ // Package paths resolves the ctx dream working locations from the // cwd-anchored project root: the gitignored dreams/ notebook and the // ideas/ source directory. The project root is the parent of the -// .context directory, by contract. +// .context directory, by contract. Resolution is pure path +// computation — it does not create directories or read their +// contents, leaving those side effects to the caller. package paths diff --git a/internal/cli/hub/cmd/revoke/cmd.go b/internal/cli/hub/cmd/revoke/cmd.go index 73adb51eb..7cf2de2aa 100644 --- a/internal/cli/hub/cmd/revoke/cmd.go +++ b/internal/cli/hub/cmd/revoke/cmd.go @@ -47,7 +47,7 @@ func Cmd() *cobra.Command { // the CTX_HUB_ADMIN_TOKEN environment variable. token := adminToken if token == "" { - token = os.Getenv(env.HubAdminToken) + token = os.Getenv(env.HubAdmin) } if token == "" { cobraCmd.SilenceUsage = true @@ -59,7 +59,7 @@ func Cmd() *cobra.Command { flagbind.StringFlag( c, &adminToken, - cFlag.Token, flag.DescKeyHubRevokeToken, + cFlag.Token, flag.DescKeyHubRevokeAuth, ) return c diff --git a/internal/cli/kb/core/topic/template.go b/internal/cli/kb/core/topic/template.go index f7bb022fb..251090cbc 100644 --- a/internal/cli/kb/core/topic/template.go +++ b/internal/cli/kb/core/topic/template.go @@ -25,7 +25,7 @@ import ( // - string: substituted body. func Substitute(body, name, slug string) string { body = strings.ReplaceAll(body, cfgKbCli.TokenTopicName, name) - body = strings.ReplaceAll(body, cfgKbCli.TokenTopicSlug, slug) + body = strings.ReplaceAll(body, cfgKbCli.TopicSlugPlaceholder, slug) body = strings.ReplaceAll(body, cfgKbCli.TokenName, name) body = strings.ReplaceAll(body, cfgKbCli.TokenSlug, slug) return body diff --git a/internal/cli/pad/core/tag/types.go b/internal/cli/pad/core/tag/types.go index 4a1fc54ed..c21db2925 100644 --- a/internal/cli/pad/core/tag/types.go +++ b/internal/cli/pad/core/tag/types.go @@ -7,6 +7,10 @@ package tag // Count holds a tag name and its occurrence count. +// +// Fields: +// - Tag: The tag name +// - Count: Number of occurrences of the tag type Count struct { Tag string `json:"tag"` Count int `json:"count"` diff --git a/internal/cli/trace/core/file/file.go b/internal/cli/trace/core/file/file.go index 72798e3f4..a1cd50f67 100644 --- a/internal/cli/trace/core/file/file.go +++ b/internal/cli/trace/core/file/file.go @@ -58,7 +58,7 @@ func ParsePathArg(arg string) string { return arg[:idx] } -// TraceFile runs git log to retrieve commits touching the given file and +// Trace runs git log to retrieve commits touching the given file and // prints context refs for each commit. // // Parameters: diff --git a/internal/cli/trace/core/show/types.go b/internal/cli/trace/core/show/types.go index 1ae9847b0..4b4cc5d86 100644 --- a/internal/cli/trace/core/show/types.go +++ b/internal/cli/trace/core/show/types.go @@ -7,6 +7,14 @@ package show // JSONRef represents a resolved context reference for JSON output. +// +// Fields: +// - Raw: The reference as written (e.g. "T-3") +// - Type: Reference kind (task, decision, learning, ...) +// - Number: Numeric ID parsed from the reference (0 if none) +// - Title: Resolved entry title (empty if not found) +// - Detail: Additional resolved detail (empty if not found) +// - Found: True when the reference resolved to a known entry type JSONRef struct { Raw string `json:"raw"` Type string `json:"type"` @@ -17,6 +25,11 @@ type JSONRef struct { } // JSONCommit represents a commit with its context refs for JSON output. +// +// Fields: +// - Commit: The commit hash +// - Message: The commit subject line +// - Refs: Context references found in the commit type JSONCommit struct { Commit string `json:"commit"` Message string `json:"message"` diff --git a/internal/config/embed/flag/hub.go b/internal/config/embed/flag/hub.go index 850f3121d..e86867dc7 100644 --- a/internal/config/embed/flag/hub.go +++ b/internal/config/embed/flag/hub.go @@ -18,7 +18,6 @@ const ( DescKeyHubStartPeers = "hub.start.peers" // DescKeyHubStopDataDir is the text key for hub stop --data-dir. DescKeyHubStopDataDir = "hub.stop.data-dir" - // DescKeyHubRevokeToken is the text key for hub revoke --token. - //nolint:gosec // G101: i18n desc key, not a credential - DescKeyHubRevokeToken = "hub.revoke.token" + // DescKeyHubRevokeAuth is the text key for hub revoke --token. + DescKeyHubRevokeAuth = "hub.revoke.token" ) diff --git a/internal/config/embed/text/err_hub.go b/internal/config/embed/text/err_hub.go index bd0b6ef4a..bec042e2b 100644 --- a/internal/config/embed/text/err_hub.go +++ b/internal/config/embed/text/err_hub.go @@ -8,9 +8,9 @@ package text // DescKeys for hub error messages. const ( - // DescKeyErrHubGenerateToken is the text key for token + // DescKeyErrHubGenerateFailed is the text key for token // generation failures. - DescKeyErrHubGenerateToken = "err.hub.generate-token" + DescKeyErrHubGenerateFailed = "err.hub.generate-token" // DescKeyErrHubInternal is the text key for internal hub // errors. DescKeyErrHubInternal = "err.hub.internal" @@ -20,9 +20,9 @@ const ( // DescKeyErrHubUnknownClient is the text key for a // revocation targeting a client ID that is not registered. DescKeyErrHubUnknownClient = "err.hub.unknown-client" - // DescKeyErrHubAdminTokenRequired is the text key for an + // DescKeyErrHubAdminRequired is the text key for an // admin-gated command invoked with no admin token supplied. - DescKeyErrHubAdminTokenRequired = "err.hub.admin-token-required" + DescKeyErrHubAdminRequired = "err.hub.admin-token-required" // DescKeyErrHubInvalidPeerAction is the text key for // unrecognized peer action errors. DescKeyErrHubInvalidPeerAction = "err.hub.invalid-peer-action" diff --git a/internal/config/embed/text/err_kb_artifacts.go b/internal/config/embed/text/err_kb_artifacts.go index c0fb92b28..b91432619 100644 --- a/internal/config/embed/text/err_kb_artifacts.go +++ b/internal/config/embed/text/err_kb_artifacts.go @@ -16,8 +16,8 @@ const ( // DescKeyErrKbGlossaryOpenFile wraps an open-for-append // failure. DescKeyErrKbGlossaryOpenFile = "err.kb.glossary.open-file" - // DescKeyErrKbGlossaryWriteRow wraps a row-write failure. - DescKeyErrKbGlossaryWriteRow = "err.kb.glossary.write-row" + // DescKeyErrKbGlossaryAppendRow wraps a row-write failure. + DescKeyErrKbGlossaryAppendRow = "err.kb.glossary.append-row" ) // Timeline writer DescKeys. @@ -30,8 +30,8 @@ const ( // DescKeyErrKbTimelineOpenFile wraps an open-for-append // failure. DescKeyErrKbTimelineOpenFile = "err.kb.timeline.open-file" - // DescKeyErrKbTimelineWriteRow wraps a row-write failure. - DescKeyErrKbTimelineWriteRow = "err.kb.timeline.write-row" + // DescKeyErrKbTimelineAppendRow wraps a row-write failure. + DescKeyErrKbTimelineAppendRow = "err.kb.timeline.append-row" ) // Source-map writer DescKeys. @@ -44,8 +44,8 @@ const ( // DescKeyErrKbSourcemapOpenFile wraps an open-for-append // failure. DescKeyErrKbSourcemapOpenFile = "err.kb.sourcemap.open-file" - // DescKeyErrKbSourcemapWriteRow wraps a row-write failure. - DescKeyErrKbSourcemapWriteRow = "err.kb.sourcemap.write-row" + // DescKeyErrKbSourcemapAppendRow wraps a row-write failure. + DescKeyErrKbSourcemapAppendRow = "err.kb.sourcemap.append-row" ) // Relationship-map writer DescKeys. @@ -59,9 +59,9 @@ const ( // DescKeyErrKbRelationshipOpenFile wraps an open-for-append // failure. DescKeyErrKbRelationshipOpenFile = "err.kb.relationship.open-file" - // DescKeyErrKbRelationshipWriteRow wraps a row-write + // DescKeyErrKbRelationshipAppendRow wraps a row-write // failure. - DescKeyErrKbRelationshipWriteRow = "err.kb.relationship.write-row" + DescKeyErrKbRelationshipAppendRow = "err.kb.relationship.append-row" ) // Contradiction writer DescKeys. @@ -75,9 +75,9 @@ const ( // DescKeyErrKbContradictionOpenFile wraps an // open-for-append failure. DescKeyErrKbContradictionOpenFile = "err.kb.contradiction.open-file" - // DescKeyErrKbContradictionWriteRow wraps a row-write + // DescKeyErrKbContradictionAppendRow wraps a row-write // failure. - DescKeyErrKbContradictionWriteRow = "err.kb.contradiction.write-row" + DescKeyErrKbContradictionAppendRow = "err.kb.contradiction.append-row" // DescKeyErrKbContradictionParseCNumber wraps a // strconv.Atoi failure on a C-### digit string. DescKeyErrKbContradictionParseCNumber = "err.kb.contradiction.parse-c-number" @@ -93,8 +93,8 @@ const ( // DescKeyErrKbDecisionOpenFile wraps an open-for-append // failure. DescKeyErrKbDecisionOpenFile = "err.kb.decision.open-file" - // DescKeyErrKbDecisionWriteRow wraps a row-write failure. - DescKeyErrKbDecisionWriteRow = "err.kb.decision.write-row" + // DescKeyErrKbDecisionAppendRow wraps a row-write failure. + DescKeyErrKbDecisionAppendRow = "err.kb.decision.append-row" // DescKeyErrKbDecisionParseDDNumber wraps a strconv.Atoi // failure on a DD-### digit string. DescKeyErrKbDecisionParseDDNumber = "err.kb.decision.parse-dd-number" @@ -110,8 +110,8 @@ const ( // DescKeyErrKbQuestionOpenFile wraps an open-for-append // failure. DescKeyErrKbQuestionOpenFile = "err.kb.question.open-file" - // DescKeyErrKbQuestionWriteRow wraps a row-write failure. - DescKeyErrKbQuestionWriteRow = "err.kb.question.write-row" + // DescKeyErrKbQuestionAppendRow wraps a row-write failure. + DescKeyErrKbQuestionAppendRow = "err.kb.question.append-row" // DescKeyErrKbQuestionParseQNumber wraps a strconv.Atoi // failure on a Q-### digit string. DescKeyErrKbQuestionParseQNumber = "err.kb.question.parse-q-number" diff --git a/internal/config/embed/text/err_kb_evidence.go b/internal/config/embed/text/err_kb_evidence.go index 7d45b66e2..01e846be4 100644 --- a/internal/config/embed/text/err_kb_evidence.go +++ b/internal/config/embed/text/err_kb_evidence.go @@ -23,9 +23,9 @@ const ( // DescKeyErrKbEvidenceOpenIndex wraps an open-for-append // failure on the evidence-index. DescKeyErrKbEvidenceOpenIndex = "err.kb.evidence.open-index" - // DescKeyErrKbEvidenceWriteRow wraps a row-write failure on + // DescKeyErrKbEvidenceAppendRow wraps a row-write failure on // the evidence-index. - DescKeyErrKbEvidenceWriteRow = "err.kb.evidence.write-row" + DescKeyErrKbEvidenceAppendRow = "err.kb.evidence.append-row" // DescKeyErrKbEvidenceParseIDNumber wraps a strconv.Atoi // failure parsing an EV-### number. DescKeyErrKbEvidenceParseIDNumber = "err.kb.evidence.parse-id-number" diff --git a/internal/config/env/doc.go b/internal/config/env/doc.go index a4cc1f89b..a93fe5ac3 100644 --- a/internal/config/env/doc.go +++ b/internal/config/env/doc.go @@ -20,7 +20,7 @@ // - Home: the user's home directory ($HOME) // - CtxDir: overrides the default .context/ // directory location ($CTX_DIR) -// - CtxTokenBudget: overrides the default token +// - CtxBudget: overrides the default token // budget for context window sizing // ($CTX_TOKEN_BUDGET) // - SessionID: active AI session identifier used diff --git a/internal/config/env/env.go b/internal/config/env/env.go index fad81a46b..b2bdca938 100644 --- a/internal/config/env/env.go +++ b/internal/config/env/env.go @@ -14,21 +14,19 @@ const ( Home = "HOME" // User is the environment variable for the current username. User = "USER" - // CtxTokenBudget is the environment variable for overriding + // CtxBudget is the environment variable for overriding // the token budget. - //nolint:gosec // G101: env var name, not a credential - CtxTokenBudget = "CTX_TOKEN_BUDGET" + CtxBudget = "CTX_TOKEN_BUDGET" // SessionID is the environment variable for the active AI session ID. // Used by ctx trace for context linking. SessionID = "CTX_SESSION_ID" // SkipPathCheck is the environment variable that skips the PATH // validation during init. Set to True in tests. SkipPathCheck = "CTX_SKIP_PATH_CHECK" - // HubAdminToken is the environment variable holding the hub + // HubAdmin is the environment variable holding the hub // admin token, used as a fallback when --token is not passed // to admin-gated commands like `ctx hub revoke`. - //nolint:gosec // G101: env var name, not a credential - HubAdminToken = "CTX_HUB_ADMIN_TOKEN" + HubAdmin = "CTX_HUB_ADMIN_TOKEN" ) // Environment toggle values. diff --git a/internal/config/handover/handover.go b/internal/config/handover/handover.go index 601389e9c..486c165c7 100644 --- a/internal/config/handover/handover.go +++ b/internal/config/handover/handover.go @@ -48,11 +48,9 @@ const FoldEntryPrefix = "- `" // mode field with a colon. const FoldEntryModePrefix = "`: mode=" -// FoldEntryPassModePrefix opens the optional pass-mode field +// FoldEntryModeAttrPrefix opens the optional pass-mode field // in a folded entry's metadata section. -// -//nolint:gosec // G101: literal markdown delimiter, not a credential -const FoldEntryPassModePrefix = ", pass-mode=" +const FoldEntryModeAttrPrefix = ", pass-mode=" // FoldEntryGeneratedAtPrefix opens the generated-at timestamp // in a folded entry's metadata section. diff --git a/internal/config/kb/cli/cli.go b/internal/config/kb/cli/cli.go index de7d10006..19d256c87 100644 --- a/internal/config/kb/cli/cli.go +++ b/internal/config/kb/cli/cli.go @@ -12,10 +12,9 @@ package cli const ( // TokenTopicName is the long-form token for the topic name. TokenTopicName = "" - // TokenTopicSlug is the long-form token for the slug. + // TopicSlugPlaceholder is the long-form token for the slug. // - //nolint:gosec // G101: angle-bracket placeholder, not a credential - TokenTopicSlug = "" + TopicSlugPlaceholder = "" // TokenName is the short-form token for the topic name. TokenName = "" // TokenSlug is the short-form token for the slug. diff --git a/internal/config/sanitize/doc.go b/internal/config/sanitize/doc.go index 5933f5370..9835fc851 100644 --- a/internal/config/sanitize/doc.go +++ b/internal/config/sanitize/doc.go @@ -7,5 +7,7 @@ // Package sanitize holds string and length constants used by the // sanitize layer. These constants define replacement characters, // special strings to detect, and maximum lengths for sanitized fields. +// Centralizing them keeps the redaction rules consistent across the +// callers that scrub secrets and oversized values before display. // Part of the internal subsystem. package sanitize diff --git a/internal/ctxctl/cli/audit/cmd/list/doc.go b/internal/ctxctl/cli/audit/cmd/list/doc.go index e234b3c91..08c094cce 100644 --- a/internal/ctxctl/cli/audit/cmd/list/doc.go +++ b/internal/ctxctl/cli/audit/cmd/list/doc.go @@ -7,5 +7,7 @@ // Package list implements `ctxctl audit list`: one row per // report in `.context/audit/`, columns are id, status, // commit-range, generated-at. A "(dismissed)" suffix on -// the status column carries the dismissal signal. +// the status column carries the dismissal signal. Reports are +// listed newest-first so the most recent audit surfaces at the +// top of the table. package list diff --git a/internal/entity/closeout.go b/internal/entity/closeout.go index bb98ab364..1d317ddd6 100644 --- a/internal/entity/closeout.go +++ b/internal/entity/closeout.go @@ -12,6 +12,14 @@ import "time" // of a closeout file. PassMode is empty for non-ingest modes // (ask, site-review, ground, note); LifeStage is empty when the // pass did not perform a topic-page synthesis. +// +// Fields: +// - SHA: Commit SHA the pass ran against +// - Branch: Git branch the pass ran on +// - Mode: Pipeline mode that produced the closeout +// - PassMode: Ingest pass-mode; empty for non-ingest modes +// - LifeStage: Topic-page life stage; empty when no synthesis ran +// - GeneratedAt: Time the closeout was written type CloseoutFrontmatter struct { SHA string `yaml:"sha"` Branch string `yaml:"branch"` @@ -24,6 +32,11 @@ type CloseoutFrontmatter struct { // CloseoutFile pairs a closeout's on-disk path with its parsed // frontmatter and the raw body bytes (everything after the // closing `---`). +// +// Fields: +// - Path: Absolute path to the closeout file +// - Frontmatter: Parsed frontmatter block +// - Body: Raw markdown body after the closing `---` type CloseoutFile struct { Path string Frontmatter CloseoutFrontmatter diff --git a/internal/entity/kb_row.go b/internal/entity/kb_row.go index 42bc7a607..3bb36b684 100644 --- a/internal/entity/kb_row.go +++ b/internal/entity/kb_row.go @@ -15,6 +15,15 @@ package entity // failures in the caller's domain-typed errors. // // Consumed by [github.com/ActiveMemory/ctx/internal/write/kb/row.Append]. +// +// Fields: +// - Header: Table header written when the artifact is created +// - NextID: Allocates the next monotonic ID from existing bytes +// - Render: Formats one markdown table row (with trailing newline) +// - ErrMkdir: Wraps a parent-directory mkdir failure +// - ErrRead: Wraps an artifact read failure +// - ErrOpen: Wraps an open-for-append failure +// - ErrWrite: Wraps a row-write failure type KBRowHooks struct { Header string NextID func(existing []byte) (string, error) diff --git a/internal/entity/trigger.go b/internal/entity/trigger.go index bfce4ba36..66f68fb60 100644 --- a/internal/entity/trigger.go +++ b/internal/entity/trigger.go @@ -30,6 +30,10 @@ type TriggerInput struct { } // TriggerSession contains session metadata sent to trigger scripts. +// +// Fields: +// - ID: The session identifier +// - Model: The active model name for the session type TriggerSession struct { ID string `json:"id"` Model string `json:"model"` diff --git a/internal/err/hub/hub.go b/internal/err/hub/hub.go index 361dd2281..e012d0b40 100644 --- a/internal/err/hub/hub.go +++ b/internal/err/hub/hub.go @@ -23,7 +23,7 @@ import ( // - error: "generate token: " func GenerateToken(cause error) error { return fmt.Errorf( - desc.Text(text.DescKeyErrHubGenerateToken), cause, + desc.Text(text.DescKeyErrHubGenerateFailed), cause, ) } @@ -77,7 +77,7 @@ func UnknownClient(id string) error { // - error: guidance on how to supply the admin token func AdminTokenRequired() error { return errors.New( - desc.Text(text.DescKeyErrHubAdminTokenRequired), + desc.Text(text.DescKeyErrHubAdminRequired), ) } diff --git a/internal/err/kb/contradiction/contradiction.go b/internal/err/kb/contradiction/contradiction.go index 18e3f1c84..a683f86ac 100644 --- a/internal/err/kb/contradiction/contradiction.go +++ b/internal/err/kb/contradiction/contradiction.go @@ -58,7 +58,7 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbContradictionWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbContradictionAppendRow), cause) } // ParseCNumber wraps a strconv.Atoi failure while parsing the diff --git a/internal/err/kb/decision/decision.go b/internal/err/kb/decision/decision.go index 95812158f..61bd5bdde 100644 --- a/internal/err/kb/decision/decision.go +++ b/internal/err/kb/decision/decision.go @@ -58,7 +58,7 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbDecisionWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbDecisionAppendRow), cause) } // ParseDDNumber wraps a strconv.Atoi failure while parsing the diff --git a/internal/err/kb/evidence/evidence.go b/internal/err/kb/evidence/evidence.go index 9d16e99a2..13e600390 100644 --- a/internal/err/kb/evidence/evidence.go +++ b/internal/err/kb/evidence/evidence.go @@ -121,5 +121,5 @@ func OpenIndex(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbEvidenceWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbEvidenceAppendRow), cause) } diff --git a/internal/err/kb/glossary/glossary.go b/internal/err/kb/glossary/glossary.go index 9854ef4c6..ee08dc2d3 100644 --- a/internal/err/kb/glossary/glossary.go +++ b/internal/err/kb/glossary/glossary.go @@ -58,5 +58,5 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbGlossaryWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbGlossaryAppendRow), cause) } diff --git a/internal/err/kb/question/question.go b/internal/err/kb/question/question.go index f8c7ac1d5..ce0fa2d12 100644 --- a/internal/err/kb/question/question.go +++ b/internal/err/kb/question/question.go @@ -58,7 +58,7 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbQuestionWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbQuestionAppendRow), cause) } // ParseQNumber wraps a strconv.Atoi failure while parsing the diff --git a/internal/err/kb/relationship/relationship.go b/internal/err/kb/relationship/relationship.go index 9b5d5b949..411ec27d8 100644 --- a/internal/err/kb/relationship/relationship.go +++ b/internal/err/kb/relationship/relationship.go @@ -58,5 +58,5 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbRelationshipWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbRelationshipAppendRow), cause) } diff --git a/internal/err/kb/sourcemap/sourcemap.go b/internal/err/kb/sourcemap/sourcemap.go index 938aa28be..95cdbeb57 100644 --- a/internal/err/kb/sourcemap/sourcemap.go +++ b/internal/err/kb/sourcemap/sourcemap.go @@ -58,5 +58,5 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbSourcemapWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbSourcemapAppendRow), cause) } diff --git a/internal/err/kb/timeline/timeline.go b/internal/err/kb/timeline/timeline.go index 500e1def1..f894fd62a 100644 --- a/internal/err/kb/timeline/timeline.go +++ b/internal/err/kb/timeline/timeline.go @@ -58,5 +58,5 @@ func OpenFile(cause error) error { // Returns: // - error: wrapped with operator-friendly prefix. func WriteRow(cause error) error { - return fmt.Errorf(desc.Text(text.DescKeyErrKbTimelineWriteRow), cause) + return fmt.Errorf(desc.Text(text.DescKeyErrKbTimelineAppendRow), cause) } diff --git a/internal/gitmeta/types.go b/internal/gitmeta/types.go index 31522b91d..4f5115963 100644 --- a/internal/gitmeta/types.go +++ b/internal/gitmeta/types.go @@ -10,6 +10,10 @@ package gitmeta // name. Branch is the literal "detached" (see // [github.com/ActiveMemory/ctx/internal/config/gitmeta.BranchDetached]) // when HEAD points at a commit instead of a symbolic ref. +// +// Fields: +// - SHA: Short commit SHA of HEAD +// - Branch: Current branch name, or "detached" when HEAD is detached type HeadRef struct { SHA string Branch string diff --git a/internal/mcp/server/dispatch/poll/loop.go b/internal/mcp/server/dispatch/poll/loop.go index 96e09f894..3494f49d5 100644 --- a/internal/mcp/server/dispatch/poll/loop.go +++ b/internal/mcp/server/dispatch/poll/loop.go @@ -10,13 +10,22 @@ import "time" // poll checks subscribed resources for mtime changes on a // fixed interval. -func (p *Poller) poll() { +// +// The stop channel is passed by value (captured under the lock +// at goroutine start) instead of read from p.pollStop each +// iteration: Stop and Unsubscribe nil that field out +// concurrently, so reading it here would be a data race. +// Closing the captured channel still unblocks the select. +// +// Parameters: +// - stop: channel closed to signal the goroutine to exit +func (p *Poller) poll(stop <-chan struct{}) { ticker := time.NewTicker(defaultPollInterval) defer ticker.Stop() for { select { - case <-p.pollStop: + case <-stop: return case <-ticker.C: p.CheckChanges() diff --git a/internal/mcp/server/dispatch/poll/poll.go b/internal/mcp/server/dispatch/poll/poll.go index ce5ab0df6..2f92d18d3 100644 --- a/internal/mcp/server/dispatch/poll/poll.go +++ b/internal/mcp/server/dispatch/poll/poll.go @@ -64,10 +64,14 @@ func (p *Poller) Subscribe(uri string) { } } - // Start poller if this is the first subscription. + // Start poller if this is the first subscription. Capture the + // stop channel in a local and hand it to the goroutine so poll + // never reads the p.pollStop field unsynchronized (Stop and + // Unsubscribe nil it out under the lock). if len(p.subs) == 1 && p.pollStop == nil { - p.pollStop = make(chan struct{}) - go p.poll() + stop := make(chan struct{}) + p.pollStop = stop + go p.poll(stop) } } diff --git a/internal/rc/load.go b/internal/rc/load.go index e57a0e6d0..e4c147e7e 100644 --- a/internal/rc/load.go +++ b/internal/rc/load.go @@ -81,7 +81,7 @@ func load() *CtxRC { logWarn.Warn(cfgWarn.ContextDirResolve, pathErr) } - if envBudget := os.Getenv(env.CtxTokenBudget); envBudget != "" { + if envBudget := os.Getenv(env.CtxBudget); envBudget != "" { budget, parseErr := strconv.Atoi(envBudget) if parseErr == nil && budget > 0 { cfg.TokenBudget = budget diff --git a/internal/rc/rc_test.go b/internal/rc/rc_test.go index 25bdfb763..199f9c94a 100644 --- a/internal/rc/rc_test.go +++ b/internal/rc/rc_test.go @@ -124,7 +124,7 @@ archive_after_days: 14 // TestGetRC_TokenBudgetEnvOverride: CTX_TOKEN_BUDGET beats .ctxrc. func TestGetRC_TokenBudgetEnvOverride(t *testing.T) { declareContext(t, `token_budget: 4000`) - t.Setenv(env.CtxTokenBudget, "2000") + t.Setenv(env.CtxBudget, "2000") Reset() rc := RC() @@ -284,7 +284,7 @@ func TestGetRC_PartialConfig(t *testing.T) { func TestGetRC_InvalidEnvBudget(t *testing.T) { declareContext(t, "") - t.Setenv(env.CtxTokenBudget, "not-a-number") + t.Setenv(env.CtxBudget, "not-a-number") Reset() rc := RC() @@ -298,7 +298,7 @@ func TestGetRC_InvalidEnvBudget(t *testing.T) { func TestGetRC_NegativeEnvBudget(t *testing.T) { declareContext(t, "") - t.Setenv(env.CtxTokenBudget, "-100") + t.Setenv(env.CtxBudget, "-100") Reset() rc := RC() diff --git a/internal/trace/types.go b/internal/trace/types.go index b3a28f52d..f2a8920e1 100644 --- a/internal/trace/types.go +++ b/internal/trace/types.go @@ -10,6 +10,10 @@ import "time" // PendingEntry records a context reference that has been staged for // attachment to the next git commit. +// +// Fields: +// - Ref: The staged context reference +// - Timestamp: When the reference was staged type PendingEntry struct { Ref string `json:"ref"` Timestamp time.Time `json:"timestamp"` @@ -17,6 +21,12 @@ type PendingEntry struct { // HistoryEntry records the context references that were attached to a // specific git commit. +// +// Fields: +// - Commit: The commit hash the refs were attached to +// - Refs: The context references attached to the commit +// - Message: The commit subject line +// - Timestamp: When the attachment was recorded type HistoryEntry struct { Commit string `json:"commit"` Refs []string `json:"refs"` @@ -26,6 +36,11 @@ type HistoryEntry struct { // OverrideEntry allows an explicit context association to be attached // to a commit after the fact, replacing any automatically recorded refs. +// +// Fields: +// - Commit: The commit hash to override refs for +// - Refs: The replacement context references +// - Timestamp: When the override was recorded type OverrideEntry struct { Commit string `json:"commit"` Refs []string `json:"refs"` @@ -34,6 +49,14 @@ type OverrideEntry struct { // ResolvedRef holds the result of resolving a raw context reference // (e.g. "T-3", "D-1", "L-5") to its full details. +// +// Fields: +// - Raw: The reference as written (e.g. "T-3") +// - Type: Reference kind (task, decision, learning, ...) +// - Number: Numeric ID parsed from the reference (0 if none) +// - Title: Resolved entry title (empty if not found) +// - Detail: Additional resolved detail (empty if not found) +// - Found: True when the reference resolved to a known entry type ResolvedRef struct { Raw string Type string diff --git a/internal/write/handover/markdown.go b/internal/write/handover/markdown.go index 031116fbc..921160841 100644 --- a/internal/write/handover/markdown.go +++ b/internal/write/handover/markdown.go @@ -89,7 +89,7 @@ func renderBody(entry Entry, folded []entity.CloseoutFile) string { sb.WriteString(cfgHandover.FoldEntryModePrefix) sb.WriteString(f.Frontmatter.Mode) if f.Frontmatter.PassMode != "" { - sb.WriteString(cfgHandover.FoldEntryPassModePrefix) + sb.WriteString(cfgHandover.FoldEntryModeAttrPrefix) sb.WriteString(f.Frontmatter.PassMode) } sb.WriteString(cfgHandover.FoldEntryGeneratedAtPrefix) diff --git a/internal/write/handover/types.go b/internal/write/handover/types.go index a1f032d1d..adc895574 100644 --- a/internal/write/handover/types.go +++ b/internal/write/handover/types.go @@ -41,6 +41,12 @@ type Entry struct { // Frontmatter holds the four required frontmatter fields of a // handover file. +// +// Fields: +// - SHA: Commit SHA the handover was written at +// - Branch: Git branch the handover was written on +// - GeneratedAt: Time the handover was written +// - Title: Human-readable handover title type Frontmatter struct { SHA string `yaml:"sha"` Branch string `yaml:"branch"` @@ -50,6 +56,11 @@ type Frontmatter struct { // File pairs a handover's on-disk path with parsed frontmatter // and the raw body bytes. +// +// Fields: +// - Path: Absolute path to the handover file +// - Frontmatter: Parsed frontmatter block +// - Body: Raw markdown body after the frontmatter type File struct { Path string Frontmatter Frontmatter