refactor: enforce six-layer package boundaries and repay the recorded debt - #2055
refactor: enforce six-layer package boundaries and repay the recorded debt#2055sang-neo03 wants to merge 36 commits into
Conversation
Add a data-driven architecture layering test that builds the full import graph (go list -json -tags authsidecar) and evaluates six rules: - extension must not depend on internal (transitive; keeps it extractable as a standalone SDK module) - events must not depend on shortcuts (transitive) - shortcuts must not directly import auth/keychain/credential/client/vfs (direct; must go through the shortcuts/common RuntimeContext gate) - cmd subpackages must not import shortcuts (assembly point + cmd/auth only) - errs must stay a leaf - internal must not depend on cmd/shortcuts/events Pre-existing violations are seeded into layering-edges.txt (37 rows). The test rejects any unseeded violation (new debt) and any stale row (removed debt), and CI locks the effective row count to only ever decrease. Removes the tautological circular-dependency check from arch-audit.yml, since Go already forbids import cycles at compile time.
Check all seven published GOOS and GOARCH combinations, and keep go list diagnostics separate from its JSON output for cold caches.\n\nMake the bootstrap snapshot immutable in CI, propagate shell failures explicitly, isolate sourced execution, and add deterministic regression tests for each contract.
Layering edge parsing and graph coverage: - Reject whitespace-padded exception fields instead of silently trimming them, so a padded row is a malformed row rather than a coerced identity; add a padded-field parse test. - Fail loud when any release target/tag combination lists zero packages, which would otherwise let the layering graph silently under-cover. - Document the build-tag scope (demo tags excluded), the SkipFrom substring semantics, and the toolchain-derived support set behind the drift check. GoReleaser drift checks: - Reject custom build commands and per-target overrides as unsupported. - Detect --tags in addition to -tags when rejecting release build tags. - Reject any GO* build environment variable (except CGO_ENABLED=0) through a single default branch instead of an explicit allowlist. - Validate the GoReleaser global env block, and make the go-list stderr test table-driven across the default and authsidecar graphs.
The extension rule skipped any package whose import path contained "/examples/", which let the gate miss two things: a directory named examples anywhere under extension escaped the rule outright, and the sanctioned demos were exempt from every denial rather than only from the internal packages they inherit through cmd. - Drop SkipFrom (and containsAny) so no rule can exempt by directory name. - Exempt the two wrapper-main demos from extension-zero-internal by exact import path. Their cmd import is the pattern they exist to demonstrate, and seeding those edges instead would wedge the ratchet: the edges track cmd's transitive set, so a new internal package under cmd would demand a new row that check-layering-ratchet.sh refuses by design. - Add examples-surface-only: demos may consume cmd and extension/platform but must not directly import internal or shortcuts. Zero violations today. layering-edges.txt stays at 39 rows, so the ratchet bootstrap snapshot still matches.
examples-surface-only promised that demos may consume only the assembled CLI and the public plugin SDK, but it enforced two denied prefixes instead, so every tree nobody thought to deny was permitted. A demo importing `events`, `errs` or a `cmd` subpackage passed the gate, and because extension-zero-internal exempts these packages from the transitive check, nothing examined what those imports dragged in either. The exemption was therefore unbounded in what it covered, the same defect as the directory-name skip it replaced. - Add Rule.AllowedRepoDeps, which inverts the check: any dependency inside this module that is not listed is a violation. Standard library and third-party packages, including same-organisation modules that are not this one, stay outside the rule. - Pin examples-surface-only to exactly `cmd` and `extension/platform`, so the rule name matches what it enforces and the inherited chain stays bounded by a direct surface of two packages. - Cover the reproducers as contract cases: other repository trees, `cmd` subpackages, other `extension` subtrees, and the module root are rejected, while the two allowed packages plus non-module imports are not. layering-edges.txt stays at 39 rows; the demos already import only the two allowed packages.
Removing the internal/core dependency left each credential provider with its own copy of the brand rule. Two identical five-line functions mean the brand set can grow in one provider and silently not in the other, with nothing to catch it at build time. Move the rule next to the Brand constants as credential.ParseBrand and have both providers call it. Same behaviour, one definition.
Once events switched to internal/imcontent directly, the convert_lib wrapper for ConvertInteractiveEventContent had no callers left. It is newly unreachable code, which the CI dead-code gate rejects because it only tolerates entries that already exist on the base branch.
Extracting the logger from keychain replaced an injected package variable with per-call construction, which regressed two things. Keychain errors went to the wrong directory. cmdutil used to inject core.GetRuntimeDir into keychain, so every auth diagnostic landed in the workspace-aware log. keychain now built its logger with empty Options, falling back to the pre-workspace ~/.lark-cli path while internal/auth kept passing core.GetRuntimeDir. Inside a workspace the two halves of one investigation split across two directories, and LARKSUITE_CLI_LOG_DIR masks it whenever that override is set. Each call also built a fresh logger. The sync.Once guarding file creation is per instance, so every logged line reopened the file — never closed — and re-ran the week-old-log prune. wrapError fires on every keychain operation, and a locked keychain is exactly the failure this log exists to diagnose. Install one logger while the command factory is built, which is the only place that knows the workspace-aware directory: authlog cannot resolve it itself because internal/core imports internal/keychain, which imports authlog. Both callers now read that shared instance, so there is one file handle and one prune per process. Tests pin the singleton and the install-wins behaviour. The process-wide variable is a stopgap: the internal/core split can hand the runtime directory to authlog directly and remove the indirection.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR adds dependency-layering enforcement, public environment-name constants, centralized message conversion and authentication logging, and internal package namespace migrations. Callers, tests, workflows, and scripts are updated accordingly. ChangesLayering quality gate
Shared environment and content contracts
Authentication logging and package boundaries
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 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 |
…stry The approved bootstrap snapshot records the registry size at the moment the file first reaches the target branch, and it is the only check that runs in that situation. This change now carries both the registry and the first round of cleanup, so the file lands on main holding 18 edges rather than the 39 it was pinned to. Update the count and hash to match, and state in a comment why the baseline stays hardcoded: a value CI could supply would let anyone raise the approved size without the change appearing in a diff.
main gained risk-control host signals, which changed the cachedHttpClientFunc and cachedLarkClientFunc signatures and rewrote the proxy-warning test around TestFactory. The only conflict was that test's import block: this branch moved the shared environment variable names out of internal/envvars into envnames, while main still imported the old package. Keep envnames for the five constants that moved and add internal/core for the config types the rewritten test now builds. internal/envvars is no longer needed here; its remaining constants are the internal-only ones.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@internal/authlog/authlog.go`:
- Around line 95-122: Update defaultRuntimeDir and Logger.logDir to validate
LARKSUITE_CLI_CONFIG_DIR and LARKSUITE_CLI_LOG_DIR with validate.SafeEnvDirPath,
and return a typed validation error when either explicit override is invalid
instead of falling back or redirecting. Adjust internal/authlog/authlog_test.go
lines 27-40 to assert the validation failure rather than the fallback directory
behavior.
- Around line 151-155: Update the argument formatting logic in authlog to avoid
persisting raw os.Args: retain command names while redacting sensitive flags and
their values, including app-secret, before joining and truncating the arguments
for the log. Ensure secrets are never emitted in the returned command string.
In `@internal/imcontent/helpers.go`:
- Around line 62-65: Update FormatTimestamp in internal/imcontent/helpers.go to
format timestamps at minute precision using the existing calendar date format,
preserving the HH:mm contract. Align the wrapper documentation or output
contract in shortcuts/im/convert_lib/helpers.go with this behavior, and add a
direct regression test covering the minute-precision result.
In `@sidecar/server-demo/handler_test.go`:
- Around line 423-428: The self-proxy validation in the run handlers must return
the prescribed typed validation/precondition error while preserving the
underlying cause, replacing the current fmt.Errorf path in both server-demo and
server-multi-tenant-demo main.go. Update TestRun_RejectsSelfProxy in
sidecar/server-demo/handler_test.go lines 423-428 and
sidecar/server-multi-tenant-demo/handler_test.go lines 419-424 to use
errs.ProblemOf and errors.As, asserting the typed error metadata and preserved
cause rather than only matching message text.
🪄 Autofix (Beta)
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: a5ee3fe3-c391-46b9-b4bd-4ef390b2df39
📒 Files selected for processing (84)
.github/workflows/arch-audit.yml.github/workflows/ci.ymlMakefilecmd/config/binder.gocmd/root_integration_test.gocmd/startup_brand.goenvnames/envnames.goevents/im/message_receive.goextension/credential/env/env.goextension/credential/env/env_test.goextension/credential/sidecar/provider.goextension/credential/sidecar/provider_test.goextension/credential/types.goextension/transport/sidecar/interceptor.gointernal/auth/app_registration.gointernal/auth/auth_response_log.gointernal/auth/device_flow.gointernal/auth/device_flow_test.gointernal/auth/revoke.gointernal/auth/uat_client.gointernal/auth/verify.gointernal/auth/verify_test.gointernal/authlog/authlog.gointernal/authlog/authlog_test.gointernal/cmdutil/factory_default.gointernal/cmdutil/factory_default_test.gointernal/cmdutil/factory_proxy_warn_test.gointernal/cmdutil/factory_test.gointernal/credential/integration_test.gointernal/envvars/envvars.gointernal/envvars/read.gointernal/envvars/read_test.gointernal/imcontent/card.gointernal/imcontent/card_test.gointernal/imcontent/card_userdsl.gointernal/imcontent/card_userdsl_test.gointernal/imcontent/convert.gointernal/imcontent/helpers.gointernal/imcontent/media.gointernal/imcontent/misc.gointernal/imcontent/text.gointernal/imcontent/text_test.gointernal/keychain/auth_log.gointernal/keychain/auth_log_test.gointernal/keychain/keychain.gointernal/openclawbind/json_pointer.gointernal/openclawbind/json_pointer_test.gointernal/openclawbind/lark_channel.gointernal/openclawbind/lark_channel_test.gointernal/openclawbind/reader.gointernal/openclawbind/reader_test.gointernal/openclawbind/secret_resolve.gointernal/openclawbind/secret_resolve_exec.gointernal/openclawbind/secret_resolve_exec_test.gointernal/openclawbind/secret_resolve_file.gointernal/openclawbind/secret_resolve_file_test.gointernal/openclawbind/secret_resolve_test.gointernal/openclawbind/tilde.gointernal/openclawbind/tilde_test.gointernal/openclawbind/types.gointernal/openclawbind/types_test.gointernal/qualitygate/deptest/layering-edges.txtinternal/qualitygate/deptest/layering_test.gointernal/secaudit/audit.gointernal/secaudit/audit_test.gointernal/secaudit/audit_unix.gointernal/secaudit/audit_windows.gointernal/secaudit/audit_windows_test.gointernal/transport/config.gointernal/transport/tls_ca.gomain_noauthsidecar.gomain_noauthsidecar_test.goscripts/check-layering-ratchet.shscripts/check-layering-ratchet.test.shscripts/ci-workflow.test.shshortcuts/im/convert_lib/content_convert.goshortcuts/im/convert_lib/content_media_misc_test.goshortcuts/im/convert_lib/helpers.goshortcuts/im/convert_lib/merge.goshortcuts/im/convert_lib/pure_adapters.gosidecar/server-demo/handler_test.gosidecar/server-demo/main.gosidecar/server-multi-tenant-demo/handler_test.gosidecar/server-multi-tenant-demo/main.go
💤 Files with no reviewable changes (4)
- internal/keychain/auth_log.go
- internal/envvars/envvars.go
- internal/keychain/auth_log_test.go
- .github/workflows/arch-audit.yml
| if len(strings.TrimLeft(timestamp, "+-")) >= 13 { | ||
| value /= 1000 | ||
| } | ||
| return time.Unix(value, 0).Local().Format("2006-01-02 15:04:05") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve the minute-precision timestamp contract.
FormatTimestamp now returns HH:mm:ss, but the migrated shortcut contract is HH:mm; calendar and todo output therefore changes during a refactor intended to centralize conversion. Restore 2006-01-02 15:04 and add a direct regression test.
internal/imcontent/helpers.go#L62-L65: format the centralized timestamp at minute precision.shortcuts/im/convert_lib/helpers.go#L30-L31: keep the wrapper’s documented output contract aligned with the centralized implementation.
As per coding guidelines, “When transcribing input or transforming requests, preserve values faithfully.”
📍 Affects 2 files
internal/imcontent/helpers.go#L62-L65(this comment)shortcuts/im/convert_lib/helpers.go#L30-L31
🤖 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 `@internal/imcontent/helpers.go` around lines 62 - 65, Update FormatTimestamp
in internal/imcontent/helpers.go to format timestamps at minute precision using
the existing calendar date format, preserving the HH:mm contract. Align the
wrapper documentation or output contract in shortcuts/im/convert_lib/helpers.go
with this behavior, and add a direct regression test covering the
minute-precision result.
Source: Coding guidelines
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@9c50045f14f2d91138f006768fe5cb739aebcb95🧩 Skill updatenpx skills add larksuite/cli#refactor/package-debt-phase2 -y -g |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2055 +/- ##
==========================================
- Coverage 75.15% 74.99% -0.17%
==========================================
Files 912 915 +3
Lines 96453 96528 +75
==========================================
- Hits 72492 72391 -101
- Misses 18383 18583 +200
+ Partials 5578 5554 -24 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Two things surfaced once CI compared this branch against main. The panic notice in cleanupOldLogs writes to os.Stderr, which forbidigo rejects. The line is unchanged from internal/keychain, but moving the file makes every line new to a diff-scoped linter. This package is a leaf with no IOStreams in scope — the same constraint defaultRuntimeDir already documents — and a panic in background cleanup still has to be visible, so mark it the way internal/output marks its equivalent stderr write. Coverage of the package sat at 28.6%: the extraction brought no test for what the logger actually writes. Add three. One pins that lines land under the supplied RuntimeDir, which is the property that regressed when a caller constructed the logger with empty options. One covers the nil-receiver guard both entry points carry. One pins the seven-day retention window and the filename patterns the prune may touch. Coverage is now 80.5%.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@internal/authlog/authlog_test.go`:
- Line 8: Replace direct filesystem calls in the authlog tests with the
corresponding internal/vfs APIs, including reads, writes, and stat operations in
the affected test sections. Remove the os filesystem dependency and ensure all
test filesystem access uses vfs so substituted vfs.DefaultFS is respected.
🪄 Autofix (Beta)
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: 43347a06-d315-4541-8e87-1afb2738299f
📒 Files selected for processing (2)
internal/authlog/authlog.gointernal/authlog/authlog_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/authlog/authlog.go
Two packages were listed in ExceptFrom, which makes the evaluator skip the source package before it looks at any dependency. gitcred needs keychain and vfs; manifest-export needs the cmd root. Exempting them wholesale also cleared every other denied import, so a later gitcred -> internal/client or manifest-export -> events would pass the gate and never reach the registry. A probe confirmed both slip through unreported. Add ExceptEdges, matched on the exact (from, denied) pair, and move these two across. ExceptFrom stays for packages whose whole job is to sit on the boundary the rule draws: the shortcuts/common runtime gate, the cmd assembly roots, the wrapper-main demos. Contract tests feed each package its allowed imports plus one denied import and assert exactly one violation, so the allowed edges carry weight instead of being asserted trivially.
The package documents one logger and one file handle per process, but SetShared overwrote the current instance on every call and the factory can be built more than once. A second construction opened a second file, left the first one open with no way to close it, and moved later lines to whichever workspace directory that construction resolved. Only the first explicit install now takes effect. A lazily created fallback is not an explicit install, so the first real one still replaces it — and closes the file it had opened, which needs the handle to be retained rather than handed to log.New and forgotten. The once-guarded init becomes a mutex so the handle can be released safely; a closed logger drops writes instead of pointing at a file nobody reads. Tests cover two non-nil installs and the fallback handover, and both fail if the guard is removed.
Removing extension's dependency on internal left the brand rule implemented twice, once per Brand type. The two cannot share code: extension is published as a standalone SDK and may not import internal, which is the constraint this branch exists to establish. Cross-reference them so a third brand is added to both rather than to whichever one the next author happens to open. Also correct the timestamp wrapper's comment, which has claimed "HH:mm" since before this branch while every implementation formatted seconds too.
The build-tag list was checked against a literal, so the test only caught edits to the list itself. A tag introduced anywhere else left its files out of every graph and the rules went quiet on them, with nothing to notice. The platform list does not have this hole: it is cross-checked against .goreleaser.yml. Walk the tree for //go:build constraints and require every custom tag to be either unioned or listed as excluded. GOOS, GOARCH and toolchain terms come from `go tool dist list` so a new port cannot look like a custom tag. Exclusions now carry a reason that is verified rather than asserted: the two demo tags are skipped because their files sit outside every rule's FromPrefix, and the test fails if a file carrying one ever lands inside one. A listed exclusion nobody uses fails too, so the list cannot rot.
…cted log dir Two behaviours carried over from internal/keychain, both left as they were when the package moved. FormatAuthCmdline kept the first three arguments. That protected secrets only while no sensitive flag appeared early: a global flag in front of the subcommand put its value straight into a file that is world-readable to the user, kept for seven days. Today's CLI cannot reach that state — the only persistent flag is --profile and secrets arrive through --app-secret-stdin — so this is about the shape, not a live leak. Drop everything from the first flag onward instead. A denylist of sensitive names would need extending whenever one is added; the command path is what the log is for, and it lives entirely in the leading non-flag arguments. args[0] is reduced to its base name so an absolute install path stays out too. logDir swallowed the error when LARKSUITE_CLI_LOG_DIR failed validation and wrote elsewhere while the caller kept watching the path they configured. Warn instead. This fires only on a rejected override, not on every run, and logDir resolves once per process. Tests cover a flag ahead of the subcommand, the absolute-path case, and that a usable override still prints nothing.
bf56e90 asked the next author to change both ParseBrand implementations by cross-referencing them. That is the weakest kind of guarantee: it holds until someone adds a brand to the package they happened to open. Assert it instead. The constants are read out of both sources rather than listed again, so a brand added to one package is under test immediately, and the two parsers are compared on every declared value plus the inputs that exercise the normalisation — case, padding, an unknown brand, a near miss. The file sits in neither parser's package so neither owns the contract. It does not merge the two implementations: extension/credential ships as a standalone SDK and may not import internal, which is the constraint this branch exists to establish.
Both demo entry points reported every startup failure with fmt.Errorf, so the self-proxy rejection was indistinguishable from a missing key file except by reading the sentence. Classify them instead: a poisoned environment and an unreadable config are ConfigError, flag validation is ValidationError, local key and log file work is InternalError, and listen or serve failures are NetworkError. Causes are attached rather than folded into the message. The self-proxy test asserts the type with errors.As and keeps the check that the message names the variable, so renaming the variable still fails the test while rewording the sentence no longer does.
…ntly LARKSUITE_CLI_LOG_DIR is validated, LARKSUITE_CLI_CONFIG_DIR is not, and the asymmetry looks like an oversight. It is not free to remove: validate.SafeEnvDirPath resolves symlinks, so routing CONFIG_DIR through it changes the directory the CLI reports on any host where the path crosses one. Applying it moved four packages' expectations from /var to /private/var on macOS. Whether config paths should be symlink-resolved is a decision about the on-disk contract, not a local tidy-up, so say so where the next reader looks. Also record why the stderr capture helper uses os while the file assertions use vfs: os.Pipe and os.Stderr are process contracts with nothing for a substituted filesystem to intercept.
475f04a stopped the command line at the first flag and dropped the "keep three words" rule with it, on the reasoning that the command path is what the log needs. That reasoning missed positional arguments: `api <method> <path>` takes the path as an argument, so a document token moved from truncated to recorded in a file that is kept for a week. Apply both limits. Stop at the first flag, so a sensitive flag ahead of the subcommand cannot slip through, and keep at most three words, so a positional identifier after the command path cannot either. Removing either one fails a test: the flag boundary alone lets the document token through, the word cap alone lets --token=... through. Verified case by case that nothing reaches the log that the pre-475f04a8 behaviour withheld.
The comment claimed the binary plus two words is the deepest command path in this CLI. It is not: generated service commands go one level further, as `drive file.comments create_v2` in the manifest tests shows, and the cap cuts their last word. Calling the bound a measurement invites the next reader to raise it for a command that does not fit — which would also admit the first positional argument, where resource identifiers live. State it as the privacy bound it is, and add the generated-command case to the table so the trade-off is visible next to the cases it protects.
97e397c classified every startup failure, including the one the config resolver had already classified. An unconfigured CLI comes back as not_configured carrying "run: lark-cli config init"; wrapping it in invalid_config put the wrong subtype in front — ProblemOf reads the outermost — and dropped the hint entirely. A caller would be told the config is broken when it was never written. Pass typed errors through untouched and reserve a fresh error for the case where the resolver gave none, where internal/unknown is the honest answer rather than a guess at invalid_config. Flag rejections now name the flag through WithParam, so a caller learns which one to fix without reading the sentence, and the tests assert subtype and param through ProblemOf instead of matching prose.
The comment sent readers to a follow-up in the pull request description that does not exist there. Keep the reason in the source, where it is already complete, and add the evidence that made the decision: applying the validator moved four packages' expectations from /var to /private/var, because it resolves symlinks.
Summary
Installs machine enforcement for the repository's six package dependency layers, then clears the reverse dependencies it records. The gate and the first round of cleanup land together: 39 existing violations are registered on arrival and 21 of them are repaid in the same change, leaving 18
shortcuts/*→internal/*edges for a later step. Supersedes #2045, which carried the enforcement half on its own.Changes
Enforcement
internal/qualitygate/deptest. It runsgo list -json ./...twice for each of the seven publishedGOOS/GOARCHcombinations — untagged and with-tags authsidecar— and unions the resulting production dependency graphs. The union matters:extension/credential/sidecarand its denied edges only exist under the tag.extensionmust not reachinternal/**(transitive);eventsmust not reachshortcuts/**(transitive); packages undershortcuts/must not importinternal/auth,internal/keychain,internal/credential,internal/clientorinternal/vfsdirectly, reaching them through theshortcuts/commonruntime gate instead — the 18 rows still registered are exactly these five; only thecmdassembly root andcmd/authmay importshortcuts/**;errsstays a repository leaf;internal/**must not import upper layers; and packages underextension/platform/examples/may directly import onlycmdandextension/platform.(from, denied)edge where a package merely needs one or two of the denied imports: a whole-package exemption would also clear every denied import added to it later, soshortcuts/apps/gitcredand the manifest-export collector use edge exemptions while the runtime gate, thecmdassembly roots and the wrapper-main demos keep whole-package ones. The two wrapper-main demos are exempt from the transitiveextensionrule because theircmdimport is the pattern they exist to demonstrate, whileexamples-surface-onlykeeps their direct surface pinned to two packages, so the inherited chain stays bounded.layering-edges.txtand reconcile in both directions: an unregistered violation fails, and a registered edge that no longer exists fails with instructions to delete the row. CI additionally compares(from, denied)key sets with the base revision, so rows can only be removed — equal-count replacement fails too. The first-run bootstrap is pinned to an approved snapshot, hardcoded so the approved size cannot be raised from CI configuration without appearing in a diff.arch-audit.yml; Go already rejects import cycles at compile time.Cleanup
extension's dependency on internal implementation (17 edges). Environment variable names move to a new repository-root leaf packageenvnames, which imports nothing: 6 of the 15 constants are read by bothextensionand internal layers, so a single home is the only way to keep one spelling per variable.internal/envvarskeeps the 5 internal-only constants, andCliAgentNameis deleted because nothing referenced it.core.ParseBrandis replaced bycredential.ParseBrand, defined next to theBrandconstants it maps to.shortcuts(4 edges).eventsreached up intoshortcuts/im/convert_libfor three functions and one context type; the transitive closure of that entry point is nowinternal/imcontent, which depends on the standard library only.convert_libkeeps the code that needs a runtime context and delegates the pure part, projecting its richerConvertContextdown at the boundary rather than duplicating the conversion.internal/keychainintointernal/authlog, constructed with explicit options instead of a replaceable package variable for the log directory. One instance is installed while the command factory is built — the only place that knows the workspace-aware runtime directory, sinceinternal/coreimportsinternal/keychain— so both callers share a single file handle and a single log prune per process.internal/bindingintointernal/secaudit(path auditing) andinternal/openclawbind(config binding and secret resolution). The halves depend one way,openclawbind→secaudit, and each existing caller uses exactly one of them.Merging
internal/appmetaintointernal/eventis deliberately out of scope: it changes no dependency and the design marks it optional.Test Plan
go build ./...andgo build -tags authsidecar ./...go test ./internal/qualitygate/deptest/ -count=1— the gate passes with 18 registered edgesbash scripts/check-layering-ratchet.sh main— passes against the approved 18-edge bootstrap snapshotbash scripts/check-layering-ratchet.sh refactor/package-topology— deletions only, so the check also holds if the enforcement half merges firstmake script-test, including the ratchet's own suite: unchanged rows, metadata-only edits, deletions and CRLF pass; new keys, equal-count replacement, malformed rows, padded fields, duplicate keys, a missing file and an invalid base revision all failgo test ./cmd/... ./internal/... ./shortcuts/... ./extension/... -count=1— 111 packages pass.internal/qualitygate/publiccontentfails in this working copy only, because a local.git/ai/subtree blockst.TempDircleanup; the same commit passes in a clean worktree and the package is untouched here.gofmt -l .andgo vet ./...report nothingorigin/main, matching the CI job, reports no newly unreachable functionsmake examples-buildstill builds both plugin-SDK demosevents/imfails withfrom,deniedandrulediagnostics; deleting an active row fails as a new violation; adding a row for a nonexistent edge fails with deletion guidance; adding a violation together with a matching row passes the Go test but fails the CI ratchetevents,errsand acmdsubpackage imported fromexamples/audit-observereach failexamples-surface-only; a new package under anyexamples/directory underextension/failsextension-zero-internalinternal/authlogtests pin that every caller observes one logger and that the installed logger wins over the fallbackRelated Issues
Summary by CodeRabbit
New Features
LARKSUITE_CLI_LOG_DIR.Bug Fixes
Quality Improvements